알고리즘

[ 알고리즘 ] 동적 계획법

Chars4785 2019. 9. 27. 14:32

@  왜 궁금한가?

 

맨날 DP DP 하는데 뭔지 해서,,, 그리고 알고리즘 문제에서 많이 나와서 한번 정리 하고 싶다는 생각,,,


@ 공부

## 동적 계획법이란 ( Dynamic Programming )

 - 부분 문제를 해결한 결과를 이용하여 전체 문제를 해결

작은 문제를 해결해서 큰 문제를 해결

> 재귀 호출

 

def getFibo(n)
	if n == 0 or n == 1 :
    	return 1;
    else : 
    	return getFibo(n-1) + getFibo(n-2);

문제점: 값을 중복해서 계산 한다.



소문제를 여러번 계산 하는 경우가 많다.

 

분할정복법을 이용한 풀이가 아님

> 문제를 독립적으로 나누지 않았기 때문

이건 단순히 작은 나를 해결한 결과를 계속해서 써먹는다. 

   F(n)  을 알기 위해서는 F(n) ~ F(n-1) 을 모두 알아야 함.

 

 

"나" 보다 작은 모든 풀이를 먼저 기억하자

 어차피 F(n)을 구하기 전에 F(1) ~ F(n-1) 을 먼저 구하자

> 이부분이 분할 정복법 하고 좀 다르다.

> 분할 정복법은 탑 다운 방식 Top Down ( 구해야 할 것을 나눠서 내려간다. )

> Dynamic Programming 은  Buttom up

 

공통 사항은: 작은 걸로 큰걸 풀자

하지만 철학이 다름

분할 정복법: Top down : 큰 문제를 쪼개 내려가는 것

동적 계획법: Buttom up : 밑에서 올라가는 것

 

## 동적 계획법의 문제 풀이 순서

1. 부분 문제를 정의 한다. ( 말로 정의 )

- 무슨 값을 구할지를 정의한다.

2. 점화식을 구한다.

- 그 값을 어떻게 구할지에 대한 식을 세운다.

- 부분은 풀려있다고 가정

3. 문제를 해결한다.

- 값을 직접 구하는 코드를 작성한다.

 

> 0 와 1 은 기저조건 ( Base condition ) 점화식으로 구할 수 없음

 

## 블럭 채우기

> 부분 문제를 정의 하는 것이 중요하다!!!!!

 

1. 부분 문제를 정의한다. 무슨 값을 구할지를 정의한다. 

> T(i) = 2* i 의 상자를 채우는 경우의 수?

> T(2) = 2

> T(3) = 3

 

2. 점화식을 구한다. 그 값을 어떻게 구할지에 대한 식을 세운다.

> T(i) =2 *i 의 상자를 채우는 경우의 수

2 * ( i-1) = T(i-1)

2 * (i-2) = T(i-2)

 

T(i) = T(i-1) + T(i-2) 

> 피보나치 수열

## 숫자 만들기

이게 먼저 동적 계획법으로 풀린다라는 감이 잡혀야 한다. 

1. 먼저 전체 경우를 생각해 줘야 하고 그 다음에 나눌수 있는지 고려 해야 한다.

5를 만들때 1 2 3 으로만 만드는 경우 

13가지 방법

2. 이 전체적인 방법을 어떻게 나눌까?

맨 끝을 주목해서 정렬 하면

5를 만들때

4를 만들어 +1 만 더하면 5가 된다.

3를 만들어 +2만 더하면 5가 된다.

2를 만들어 +3만 더하면 5가 된다.

즉 큰문제를 알기 위해서 작은 문제를 알고 있어야 한다.

## 풀이

1. 부분 문제를 정의한다.

2. 점화식을 구한다.

 

3. 문제를 해결한다.

하나하나 배열에 넣어 준다.

점화식을 풀기 위해서 T(0) 을 생각해 줘야 한다.

그런데 1~3의 수를 이용하여 0 을 만드는 경우의 수는 뭔가? 말이 안된다. 나머지를 생각해서 배열에 넣게 되면 된다. 단 점화식으로 구할 수 없는 것은 머리로 구해 줘야 한다.

T(0) = x

T(1)  = 1

T(2) = 2

T(3) = 4

경우의 수이기 때문에  더하기라고 생각하면 안된다. 

T(5) = T(4) +1 +T(3) + 2 + T(2) + 3 이라고 생각하면 안된다. 경우의 수이기 때문에 값을 구하는 것이 아니고 경우를 구하는 것이다. +3 은 5를 만들기 위해서 +3을 했다는 것이지 카운트 하는 것에는 전혀 영향을 안주기 때문에 말이 안되는 수식이다.!!

## 코드

public class DPmakeNumber {
	static int MAX = 100;
	
	public static void main(String[] args) {
		int[] Table = new int[MAX];
		
		Scanner scanner = new Scanner(System.in);
		int n = scanner.nextInt();
		int m = scanner.nextInt();
		Table[1] = 1;
		int sum =0;
		
		/*
		 * M =4
		 * Table[1] =1
		 * Table[2] = Table[1] +1
		 * Table[3] = Table[2]+ Table[1]+1
		 * Table[4] = Table[3]+Table[2]+ Table[1] +1 
		 */
		
		for (int i = 2; i <= m; i++) {
			sum += Table[i-1];
			Table[i] = sum +1;
		}
		
		for (int i = m+1; i <= n; i++) {
			for(int j=i-m;j<=i-1;j++) {
				Table[i] += Table[j];
			}
		}
		
		System.out.println(Table[n]);
	}
}

코드는 쉽다 하지만 저기까지의 생각이 도달하기 어렵다.

 

## 연속 부분 최대합

 

각각의 경우 오른쪽 끝으로 했을때 그때 가장 큰 값을 구해 본다. 

          5 : 5

     -4 5 : 1

  2 -4 5 : 3

1 2 -4 5 : 4

다 더했을 때 5 혼자 있는게 가장 큰 값이 구나 > 동적 계획법을 사용하면 좀 더 빠르게 구할 수 있다.

     -4

  2 -4

1 2 -4 

-4를 끝으로 한 경우에 가장 큰 경우만 생각한다면 3가지 경우 중에서 한가지만 알면 된다.

즉 

   5

-1 5

이렇게 두가지만 생각하면 된다.

## 풀이

1. 부분 문제를 정의한다.

T(i) = i 번째 숫자를 오른쪽 끝으로 하는 연속 부분 최대합

 

2. 점화식을 구한다.

즉, 예를 들어 

1  2   3  4  5   6   7  8 

1, 2, -4, 5, 3, -2, 9, 10 이면 -2을 끝으로 하는 것은

T(5) -2 또는 (-2) 

둘중 하나가 되기 때문에  

 

T(i) = max(T(i-1) + Data(i), Data(i) )

 

3. 문제를 해결한다.

T(0)은 점화식으로 못구한다. 그냥 계산 T(0) = 1

출력시 10일때가 맨 오른쪽일때 가장 최대값을 갖는 구나라고 이야기 할 수 있다.

중간에 최대 값이 나올 수도 있다. 만약에 마지막이 -100 이였다면 25가 아닌 -85가 들어가게 된다. 그럼 10을 오른쪽 끝으로 하는 것이 최대가 아니고 9 가 오른쪽 끝일때가 가장 큰 값이 된다. 

 

즉, 가장 큰 값을 출력 해야 한다. => 그래서 이것이 빠른가?

 

테이블 채우는데 O(N) 이 걸리게 된다. => 제일 빠르다 그리고 코드가 짧다 !!!

 

## 구현

public class DP_sumFindMax {

	// 1. Table 정의
	// 2. 점화식

	// Table(i) = i 번째 숫자를 오른쪽 끝으로 하는 연속 부분 최대합
	// Table(i) = max( Table(i-1) + Data(i), Data(i))
	static int MAX = 100;

	public static void main(String[] args) {
		int[] Table = new int[MAX];
		int[] Data = new int[MAX];
		
		Scanner sc = new Scanner(System.in);
		int n = sc.nextInt();
		
		for(int i=0;i<n;i++) {
			Data[i] = sc.nextInt();
		}
		
		Table[0] = Data[0];
		
		for(int i=1;i<n;i++) {
			if(Table[i-1]+Data[i] > Data[i]) {
				Table[i] = Table[i-1] + Data[i];
			}else {
				Table[i] = Data[i];
			}
		}
		
		int myMax = -9769928;
		
		for (int i = 0; i < n; i++) {
			if(Table[i] > myMax) {
				myMax = Table[i];
			}
		}
		
		System.out.println(myMax);
				
	}

}

## 팰린드롬 만들기

palindrome이란, 그냥 읽었을 때와 거꾸로 읽었을 때가 같은 문자열이다.

palindrome 을 만들기 위한 최소 추가 문자 개수를 출력하라.

 

 

 

 

마무리