Dynamic Programming(DP)
문제에 대한 정답이 될 가능성이 있는 모든 해결책을 "체계적"이고 "효율적"으로 탐색하는 풀이법
- 크고 복잡한 문제를 작은 문제들로 나눈다.
- 하위 문제의 답을 계산한다.
- 중복 계산해야 하는 하위 문제가 있다. (overlapping subproblems - 중복 하위 문제)
- 한 번 계산한 결과는 메모리에 저장하여 재계산하지 않도록 한다. => 속도향상 (memoization, dp table)
- Top Down 방식에 저장하는 메모리 : memoization
- Bottom Up 방식에 저장하는 메모리 : dp table
- 하위 문제에 대한 답을 통해 원래 문제에 대한 답을 계산한다. (optimal substructure - 최적 부분 구조)
- 최적 부분 구조 : 하위 부분 문제에서 구한 최적의 답이 합쳐진 큰 문제의 최적의 답을 구할 수 있는 구조
Top Down vs Bottom Up
- Top Down : 큰 문제를 작은 문제로 나누어 해결하는 방법
- Bottom Up : 작은 문제부터 차근차근 답을 도출하는 방법
삽질일기
대충 강의 듣다가 fibonacci 수열을 dp로 어떻게 구현해야할지 떠올라서 구현을 해보았다.
memory = {}
def fibo_dp(n) :
if n == 1 or n == 2 :
return 1
if n-1 in memory :
memory[n] = memory[n-1] + fibo_dp(n-2)
return memory[n]
elif n-2 in memory :
memory[n] = memory[n-2] + fibo_dp(n-1)
return memory[n]
elif n-2 in memory and n-1 in memory :
memory[n] = memory[n-1] + memory[n-2]
return memory[n]
memory[n] = fibo_dp(n-1) + fibo_dp(n-2)
return memory[n]
짜잔🌟
구현하고보니 코드가 더러웠다. 그래서 조금 중복되는 부분을 정리해줬다.
memory = {}
def fibo_dp(n) :
if n == 1 or n == 2 :
return 1
if n-2 in memory and n-1 in memory :
memory[n] = memory[n-1] + memory[n-2]
elif n-2 in memory :
memory[n] = memory[n-2] + fibo_dp(n-1)
elif n-1 in memory :
memory[n] = memory[n-1] + fibo_dp(n-2)
else :
memory[n] = fibo_dp(n-1) + fibo_dp(n-2)
return memory[n]
그리고 강사님의 솔루션은 다음과 같았다.
def fibo_dp(n) :
if n == 1 or n == 2 :
return 1
if n not in memory :
memory[n] = fibo(n-1) + fibo(n-2)
return memory[n]
근데 이 두 코드를 실행해보고나니 실행 속도가 강사님이 설명해주신 코드가 더 빨랐던 것이다.
디버깅도 해보고 몇몇 수정을 거쳤지만(처음에는 fibo_dp 함수가 아니라 fibo 함수를 호출하고 있었고 다음 함수를 호출하기 전에 memory를 초기화해주는 부분도 없었다.) 여전히 강사님이 구현한 함수가 더 실행속도가 빨랐다.
내 추측은 아니고 다른 분의 추측은 if ~ else 구문을 통과하면서 검색을 3번이나 하기 때문에 거기에서 발생하는 비용으로 인해서 속도가 느려진다는 것이었고 나도 그 부분에 어느정도 동의를 했다. Python의 Hash Search는 N(1)의 시간복잡도를 가지지만 그것도 역시 시간복잡도긴 하니까.
배운점
- 일단
검색에도 비용이 든다.
라는 중요한 교훈을 얻었다. 문제를 풀 때 보통 정렬까지는 시간비용을 계산하면서 검색은 시간비용을 계산하지 않는다는 것을 깨달았다. 벤 다이어 그램을 잘 생각해봐야 할 것 같다
는 생각이 들었다. 예를 들면 첫 번째 구현에서 해당하는 부분은
"else에 해당하는 부분이 조건1이거나 조건2이거나 조건1과 조건2를 충족하지 못하는 것"인데, 이는 다른 말로 하면 "조건1도 조건2도 아닌 것"이 된다.
elif n-2 in memory :
memory[n] = memory[n-2] + fibo_dp(n-1)
elif n-1 in memory :
memory[n] = memory[n-1] + fibo_dp(n-2)
내가 예외로 발생할 수 있다고 생각한 이 부분에서 디버그 포인트를 찍어보면 1번 밖에 실행이 되지 않는다. 그 1번은 n이 4일 경우, 즉 3은 들어있는데 2는 들어있지 않는 경우뿐이다. (1과 2는 메모리에 넣어주지 않으므로)
결론적으로 if n not in memory :
라는 조건문과 내가 사용한 4중 if ~ else문은 동일한 동작을 하게 된다.
- 조건문을 짤 때 첫 번째 조건에 유의하자.
사실 내가 처음에 짯던 코드의 조건문은
if n-1 in memory :
elif n-2 in memory :
elif n-2 in memory and n-1 in memory :
else :
이거였다. 즉,
elif n-2 in memory and n-1 in memory :
이 조건은 들어올 일이 없는 것이다. 조건문은 넓은 범위 -> 좁은 범위이 아니라🙅 좁은 범위 -> 넓은 범위
가 되어야 한다.