편집을 취소할 수 있습니다. 이 편집을 되돌리려면 아래의 바뀐 내용을 확인한 후 게시해주세요.
최신판 | 당신의 편집 | ||
1번째 줄: | 1번째 줄: | ||
Dynamic Programming, | Dynamic Programming, DP. | ||
== 개요 == | == 개요 == | ||
동적 계획법은 어떤 최적화 문제를, 작은 부분 문제로 나누어 차례대로 해결하는 알고리즘을 말한다. | |||
이름이 Dynamic Programming이지만 그닥 역동적인 알고리즘은 아닌데, [[예산]]을 따오기 위해 멋진 이름을 지었다고 카더라(...). 또한 Programming은 컴퓨터 프로그래밍을 의미하는 것이 아니고, [[선형계획법]]처럼 최적화 문제의 해결 방법이라는 의미이다. | 이름이 Dynamic Programming이지만 그닥 역동적인 알고리즘은 아닌데, [[예산]]을 따오기 위해 멋진 이름을 지었다고 카더라(...). 또한 Programming은 컴퓨터 프로그래밍을 의미하는 것이 아니고, [[선형계획법]]처럼 최적화 문제의 해결 방법이라는 의미이다. | ||
== 예시 == | == 예시 == | ||
35번째 줄: | 29번째 줄: | ||
저 과정에서 <math> \mathrm{fib}(2) </math>는 3번, <math> \mathrm{fib}(3) </math>는 2번 계산됨을 알 수 있다. 이걸 일일히 계산하지 말고, 한번 구한 값을 그냥 어디 적어(memo)놨다가 다시 쓰면 어떨까? | 저 과정에서 <math> \mathrm{fib}(2) </math>는 3번, <math> \mathrm{fib}(3) </math>는 2번 계산됨을 알 수 있다. 이걸 일일히 계산하지 말고, 한번 구한 값을 그냥 어디 적어(memo)놨다가 다시 쓰면 어떨까? | ||
이 기법이 바로 | 이 기법이 바로 메모화(memoization)이다. | ||
저 그림의 맨 왼쪽 아래부터 계산하기 시작해보자. <math> \mathrm{fib}(2) = 1 + 0 = 1 </math>로 채워진다. 그 위의 <math> \mathrm{fib}(3) = 1 + 1 = 2 </math>가 될 것이다. 이제 <math> \mathrm{fib}(4) </math>를 계산해야 한다. 원래는 <math> \mathrm{fib}(4) = 2 + \mathrm{fib}(2) </math>로 오른쪽의 <math> \mathrm{fib}(2) </math>를 다시 계산해야겠지만, 앞서 <math> \mathrm{fib}(2) </math>를 계산할 때 어디 적어뒀다면 이를 바로 읽어와 <math> \mathrm{fib}(4) = 2 + 1 = 3 </math>을 간단히 계산할 수 있다. 마찬가지로, <math> \mathrm{fib}(5) </math>의 계산 역시 <math> \mathrm{fib}(3) </math>의 계산 결과가 저장되어 있다면, 곧바로 <math> \mathrm{fib}(5) = 3 + 2 = 5 </math>라는 값을 구할 수 있다. 메모하지 않았다면 <math> \mathrm{fib}(3), \mathrm{fib}(2) </math> 따위를 여러번 다시 구해야 할 판인데, 계산이 확 줄어들었다. | 저 그림의 맨 왼쪽 아래부터 계산하기 시작해보자. <math> \mathrm{fib}(2) = 1 + 0 = 1 </math>로 채워진다. 그 위의 <math> \mathrm{fib}(3) = 1 + 1 = 2 </math>가 될 것이다. 이제 <math> \mathrm{fib}(4) </math>를 계산해야 한다. 원래는 <math> \mathrm{fib}(4) = 2 + \mathrm{fib}(2) </math>로 오른쪽의 <math> \mathrm{fib}(2) </math>를 다시 계산해야겠지만, 앞서 <math> \mathrm{fib}(2) </math>를 계산할 때 어디 적어뒀다면 이를 바로 읽어와 <math> \mathrm{fib}(4) = 2 + 1 = 3 </math>을 간단히 계산할 수 있다. 마찬가지로, <math> \mathrm{fib}(5) </math>의 계산 역시 <math> \mathrm{fib}(3) </math>의 계산 결과가 저장되어 있다면, 곧바로 <math> \mathrm{fib}(5) = 3 + 2 = 5 </math>라는 값을 구할 수 있다. 메모하지 않았다면 <math> \mathrm{fib}(3), \mathrm{fib}(2) </math> 따위를 여러번 다시 구해야 할 판인데, 계산이 확 줄어들었다. | ||
52번째 줄: | 46번째 줄: | ||
</pre> | </pre> | ||
==== 시간 복잡도 분석 ==== | ==== 시간 복잡도 분석 ==== | ||
우선 | 우선 메모화를 적용하지 않고 계산했을 때의 시간 복잡도를 구해보자. <math> \mathrm{fib}(n) </math>을 계산하기 위해서는, <math> \mathrm{fib}(n - 1) </math>과 <math> \mathrm{fib}(n - 2) </math>를 계산해야 한다. 계산 시간을 <math> T(n) </math> 이라고 하면, <math> T(n) = T(n - 1) + T(n - 2) </math> 이다. 이는 피보나치 수열의 점화식과 같다. [[big-O 표기법]]에 따라 쓰면 <math> O(\mathrm{fib}(n)) </math>이고, 좀 큰 <math> n </math> 에 대해 <math> \mathrm{fib}(n) \approx 1.6^n </math> 이므로, <math> O(1.6^n) </math>로 쓸 수 있다. | ||
지수함수라는 놈이 얼마나 흉악한 놈인지는 다들 알겠지만 한번 짚어보자. <math> n = 100 </math>정도만 생각해봐도 대충 <math> 1.6^{100} = 2.6 \times 10^{20} </math>쯤 되는데, 이건 컴퓨터가 뚝딱 계산할 수 있는 양이 아니다. 요즘 CPU 클럭수가 대략 4GHz, 초당 <math> 4 \times 10^9 </math>회니까, 65,000,000,000초, 752314일쯤 걸리지 않을까? 물론 시간복잡도에 숫자 때려넣은 걸 클럭수로 나눈 것과 실제 수행 시간이 일치하지는 않지만, 대충 감은 잡을 수 있다. | 지수함수라는 놈이 얼마나 흉악한 놈인지는 다들 알겠지만 한번 짚어보자. <math> n = 100 </math>정도만 생각해봐도 대충 <math> 1.6^{100} = 2.6 \times 10^{20} </math>쯤 되는데, 이건 컴퓨터가 뚝딱 계산할 수 있는 양이 아니다. 요즘 CPU 클럭수가 대략 4GHz, 초당 <math> 4 \times 10^9 </math>회니까, 65,000,000,000초, 752314일쯤 걸리지 않을까? 물론 시간복잡도에 숫자 때려넣은 걸 클럭수로 나눈 것과 실제 수행 시간이 일치하지는 않지만, 대충 감은 잡을 수 있다. | ||
그렇다면 | 그렇다면 메모화를 적용했을 때는 시간이 얼마나 걸릴까? 이미 계산한 값은 다시 계산하지 않기 때문에, n이 1 증가했을 때 추가되는 시간은 앞서 계산한 <math> \mathrm{fib}(n - 1) </math>과 <math> \mathrm{fib}(n - 2) </math>의 합을 구하는 시간만 필요하다. 이는 상수 시간에 해결될 수 있으므로, <math> T(n) = T(n - 1) + c </math> 이고, 결국 <math> T(n) = O(n) </math>이다. 지수 시간이 걸리는 문제를, 동적 계획법을 이용해 다항시간에 해결할 수 있음을 알 수 있다. | ||
==== Bottom-up Approach ==== | ==== Bottom-up Approach ==== | ||
메모화를 통해 빠르게 피보나치 수열의 값을 구할 수 있게 되었다. 그런데 문제가 하나 남았다. 저 구현이 아직 [[재귀함수]]의 형태를 띠고 있다는 것이다. 재귀함수는 디버깅이 힘들고, 무엇보다 호출 스택의 한계 때문에 일정 이상으로 재귀하게 되면 프로그램이 터져 버리는 문제가 발생할 가능성이 있다. 모든 재귀함수는 반복문으로 고칠 수 있다는 사실이 증명되어 있으므로, 저걸 반복문으로 고치는게 좋겠다. | |||
메모화 재귀에서는 <math> \mathrm{fib}(5) </math>를 먼저 호출하고, 그 곳에서 <math> \mathrm{fib}(4), \mathrm{fib}(3), \cdots </math>등을 위에서 아래로 (Top-down) 호출하게 된다. 하지만 실제로 피보나치 수를 저장한 테이블이 차오르는 순서는 반대로, 아래에서부터 올라오게 된다. 위에서 아래로 내려가는 과정을 생략하고, 그냥 아래서부터 채워 올라오는 것이 바로 Bottom-up Approach이다. | |||
<pre> | <pre> | ||
84번째 줄: | 78번째 줄: | ||
</pre> | </pre> | ||
처럼 하면 메모리 공간을 절약할 수 있다. 다만 이 풀이는 | 처럼 하면 메모리 공간을 절약할 수 있다. 다만 이 풀이는 동적계획법적인 풀이라고 보기는 힘들고, 피보나치 수열을 구하는 수많은 방법 중 하나라고 생각하자. 동적 계획법은 메모리를 희생해서 수행 시간을 개선하는 방법이라고 생각해도 좋다. | ||
=== 거스름돈 문제 === | === 거스름돈 문제 === |