Study/알고리즘

Java: 동적 계획법(Dynamic Programming) 개념과 이해

devyoseph 2021. 10. 31. 00:19

 

동적 계획법

Dynamic Programming 또는 DP라고 부른다

 

1940년대 리차드 벨만이 사용하던 용어이다.

어떤 문제가 주어질 때 본래의 문제를 분석하고 반복되는 연산을 찾아낸다

연산을 기준으로 문제를 작은 문제들로 나눈 다음

각 문제들의 결과값을 기록하며 본래의 문제의 해답을 구하는 방법이다

 

단계를 구체화하면 다음과 같다

문제 ➡ 점화식 발견 ➡ 작은 문제들로 나누기 ➡ 각 문제의 해답을 기록 ➡ 해답

 

 

피보나치 수열

피보나치 수열을 통해 동적계획법을 이해한다

 

점화식 발견, 연산한 값을 기록하는 것

dp의 핵심이라 볼 수 있다

 

피보나치 수열의 0항과 1항은 1이고 n항은 다음과 같이 표현된다

f(n) = f(n-1) + f(n-2)

 

dp를 사용하지 않고 피보나치 특정항을 구해보자

40항을 구하려고 한다. 반복문 or 메소드를 사용하면

컴퓨터는 아래처럼 연산할 것이다

1 1 2 3 5 8 13 21 34 ...

뚜루루...40항은 165580141

 

근데 갑자기 41항을 또 구하고 싶어졌다

컴퓨터는 또 처음부터 연산한다

1 1 2 3 5 8 13 21 34 ...

위 경우 비효율적으로 연산을 반복한다

 

동적 계획법을 통해 피보나치 수열을 구해보자

1) 값을 기록하기 위해 dp[ ] 배열을 만든다

2) n항의 값을 배열 안에 기록한다

3) 기록된 n항과 n-1항을 토대로 n+1항을 구한다

4) while문 안에서 피보나치의 여러 항을 구할 수 있다

import java.util.Scanner;
public class Main {
	static Integer[]dp = new Integer[42]; //null을 사용하기 위해  Integer을 사용한다

	static Integer fibonacci(int N) {
	
		if(dp[N]==null) { //값이 비어있는 경우에만 연산을 한다
		
			dp[N]= fibonacci(N-1) + fibonacci(N-2);//연산 후 배열 안에 저장한다
	
		}
		
		return dp[N]; //dp[N]값을 반환한다
	}
	
public static void main(String[] args){
	 Scanner sc = new Scanner(System.in);
	 dp[0] = 1; //0항을 넣어준다
	 dp[1] = 1; //1항을 넣어준다
	 
	 while(true) { //while문 안에서 여러 번 피보나치 해를 구한다
		 int N = sc.nextInt();
		 System.out.println(fibonacci(N));
	 }

}}

*배열을 통해 연산 중간 중간을 기록하면 피보나치 수열을 구할 때 매번 재연산할 필요가 없어진다

 

 

동적 계획법의 구현

연산의 방향에 따라 동적 계획법의 구현 방법이 나눠진다

 

메모이제이션(Memoization)

위처럼 동일한 문제를 반복할 때 이미 계산을 마친 값들을

저장하고 활용하는 방식을 메모이제이션이라고 한다.

동적 계획법을 구현시 핵심적인 요소이다

 

Top-Down

메소드의 재귀를 이용해 풀이하는 방식

 

위의 경우처럼 피보나치 메소드(N)를 만들고

return값으로 자기 자신의 메소드(N-1)을 호출한다.

호출한 메소드(N-1)은 연쇄적으로 메소드를 호출하게 된다

이미 연산을 마친 후 배열에 기록되어있다면 재연산하지 않도록 한다

메소드 특성상 유지보수가 Bottom-Up보다 용이하다

static Integer fibonacci(int N) {
	
		if(dp[N]==null) { //값이 비어있는 경우에만 연산을 한다
		
			dp[N]= fibonacci(N-1) + fibonacci(N-2);//연산 후 배열 안에 저장한다
	
		}
		
		return dp[N]; //dp[N]값을 반환한다
	}

 

 

Bottom-Up

반복문을 통해 i의 0부터 N(또는 N-1)까지 연산한다

메소드를 만들지 않고 반복문을 활용해

연산의 결과값을 누적해간다.

코드길이나 논리가 Top-Bottom에 비해 단순하다

import java.util.Scanner;
public class Main {
	static Integer[]dp = new Integer[42]; //null을 사용하기 위해  Integer을 사용한다
public static void main(String[] args){
	 Scanner sc = new Scanner(System.in);
	 dp[0] = 1; //0항을 넣어준다
	 dp[1] = 1; //1항을 넣어준다
	 
	 while(true) { //while문 안에서 여러 번 피보나치 해를 구한다
		 int N = sc.nextInt();
		 
		 for(int i=0; i<=N; i++) {
			 if(dp[i]==null) {  //배열에 값이 없을 때만 연산한다
				 dp[i]=dp[i-1]+dp[i-2];
			 }
		 }
		 System.out.println(dp[N]);
	 }

}}

 

 

 

동적 계획법 대표 유형

 

LIS

수열 A가 주어졌을 때, 가장 긴 증가하는 부분 수열을 구하는 프로그램을 작성하시오.(최대길이 1,000)

예를 들어, 수열 A = {10, 20, 10, 30, 20, 50} 인 경우에 가장 긴 증가하는 부분 수열은

A = {10, 20, 10, 30, 20, 50} 이고, 길이는 4이다.

 

LCS

LCS(Longest Common Subsequence, 최장 공통 부분 수열)문제는 두 수열이 주어졌을 때, 모두의 부분 수열이 되는 수열 중

가장 긴 것을 찾는 문제이다. 예를 들어, ACAYKP와 CAPCAK의 LCS는 ACAK가 된다. (최대 1,000자)

 

부분수열 최대합

계단 오르는 데는 다음과 같은 규칙이 있다.

계단은 한 번에 한 계단씩 또는 두 계단씩 오를 수 있다. 즉, 한 계단을 밟으면서 이어서 다음 계단이나, 다음 다음 계단으로 오를 수 있다.

연속된 세 개의 계단을 모두 밟아서는 안 된다. 단, 시작점은 계단에 포함되지 않는다. 마지막 도착 계단은 반드시 밟아야 한다.

따라서 첫 번째 계단을 밟고 이어 두 번째 계단이나, 세 번째 계단으로 오를 수 있다.

하지만, 첫 번째 계단을 밟고 이어 네 번째 계단으로 올라가거나,

첫 번째, 두 번째, 세 번째 계단을 연속해서 모두 밟을 수는 없다. 각 계단에 쓰여 있는 점수가 주어질 때

이 게임에서 얻을 수 있는 총 점수의 최댓값을 구하는 프로그램을 작성하시오.

 

예를 들어 <그림 2>와 같이 시작점에서부터 첫 번째, 두 번째, 네 번째, 여섯 번째 계단을 밟아 도착점에 도달하면

총 점수는 10 + 20 + 25 + 20 = 75점이 된다.

 

Knapsack