어찌보니 Leetcode 프리미엄이 1년치가 구독이 되어져버렸다.. (취소를 못한 나의잘못..)
여튼 그래서 이번에 공부를 좀 해보고자 한다.
Dynamic Programming(DP)에 대해서 공부한지가 너무 가물가물하니 예시 문제를 풀면서 Github에도 업로드도 하면서 복습하면 좋은 시간이 되어지지 않을까 싶다!
DP를 이해하기 앞서서 Recursion과 Greedy algorithm에 대해서 알아야하며 Big O와 hash tables에 대해서도 알아야 하며 잘 모를경우 Recursion먼저 배워야한다.
What is Dynamic Programming ?
DP가 무엇인가에 대해서 문게에 대해서 가능한 모든 방법을 탐색해서 해결하는 방식을 말하는데.. 이는 직관적으로 이해가 되지 않는다. 그래서 2가지의 특성을 가지면 이해할수 있는데 아래와 같다.
- 여러번 재 반복해서 더 작은 문제로 풀수 있는 방법인가('overlapping subproblems')
- 이러한 overlapping subproblems이 최적의 방법으로 풀수 있는가 ('optimal substructures')
대표적인 예시의 문제로 피보나치 수열의 문제가 있는데 이는 DP에 대해서 설명을 하여준다. 처음 시작은 0,1로 시작을 하고 다음에서는 이전에 정의되었던 0,1의 합으로 다음 수가 만들어지는 원리이다.
위의 그림처럼 $n^{th}$의 경우에서는 피보나치 수를 $F(n)$으로 정의를 하는데 방법을 subproblems로 나눌수 있다.즉 $F(n-1) + F(n-2)$로 나뉘어진다. 그리고 두 수식은 optimal substructure로 표현이 되어지는 $F(n) = F(n-1) + F(n-2)$로 더해진다. 이 subproblems은 overlapping이 되어진다.
위와 같은 방법은 greedy problems과 유사한데 optimal substructure를 가진다는 점이다. 하지만 overlapping(겹치지) 하지는 않다. 다른 방법중에 하나인 divide and conquer의 방법도 비슷하지만 overlapping하지 않는 점이 다르다고 볼 수 있다.
Top-down and Bottom-up
이러한 DP는 2가지의 방법으로 풀수 있으며 아래처럼 알려져있다.
- bottom-up 은 tabulation
- top-down은 memoization
Bottom-up(Tabulation)
이 방법의 경우에서는 가장 base case에서부터 시작되는 방법이다. 피보나치로 예시로 들었을 때 $F(0)=0, F(1)=1$를 사용하여 $F(3)$을 만들고 나중에 $F(n)$가지 만드는 과정을 말한다.
// Pseudocode example for bottom-up
F = array of length (n + 1)
F[0] = 0
F[1] = 1
for i from 2 to n:
F[i] = F[i - 1] + F[i - 2]
Top-down (Memoizaion)
이 방법의 경우는 recursion사용하고 효과적으로 memoization을 만들어진다. 예시로 $n^{th}$의 경우 $F(n)$의 피보나치 수를 가지며 이는 $F(n-1), F(n-2)$로 구성이 되어지는것을 찾을 수 있다. 이는 recusive 패턴을 찾아 $F(0) = F(1) = 1$이라는 것을 찾을 수 있다.
반복적인 패턴으로 인해서 $F(100)$같의 값을 찾기 위해서는 이전의 $F(99), ... F(0)$까지 값을 계산해야되는데 이는 비효율적이다. 결국 빠르게 찾기 찾기 위해서 memoize의 결과를 활용하고 이를 memoizion이라고 불린다.
**memoizion**는 결과의 값을 hashmap, array에 저장을 해 놓고 같은 값이 나오면 return을 하는 방법이다.
코드는 아래와 같다.
// Pseudocode example for top-down
memo = hashmap
Function F(integer i):
if i is 0 or 1:
return i
if i doesn't exist in memo:
memo[i] = F(i - 1) + F(i - 2)
return memo[i]
둘중에 누가 좋나?
- bottom up의 경우에서는 runtime이 빠르다. 왜냐! recursive를 하지 않기 때문에
- top-down은 더 쉽게 코드 작성이 가능하며 recursion을 사용하지 않음.
When to Use DP
특히나 DP의 문제에서는 코테에서 문제를 푸는 방법을 찾는데 시간을 절반을 소요하기 때문에 앞서 말한 2가지의 특징이 중요합니다.
- overlapping subproblems : 작은 문제를 여러번 사용이 가능해야됨
- optimal substructure : 중첩되는 즉 overlapping의 문제를 최적의 방법으로 풀어야한다.
이렇게 말하면 뭔가 느낌적으로 감이 안올것이기 때문에 좀더 쉽게 몇가지의 특징에 대해서 말해보겠습니다.
First characteristic
어떤 문제에 있어서 optimum value를 요구합니다.예로들어서 가장 cost가 적게드는 maximum한 값, 몇가지의 방법이 있는가..등등
모든 방법들이 위의 예제같이 나타나진 않지만 공통된 특징을 가지고 있습니다.
첫번째의 방법은 설명한 방법을 무조건 DP로 풀진 않아도 됩니다. (Greedy algorithm 으로도 풀수 있음)
Second characteristic
DP의 다른 특징중에 하나는 미래의 decisions이 과거의 Decision에 영향을 가지는 특징을 가지고 있습니다.
마치 나비효과처럼 처음의 값이 나중에 값의 영향을 미치는것입니다.
이 특징은 greedy algorithm에서는 적용할수 없는 문제이기때문에 DP로만 풀수 있습니다.
0. 예시 문제 : Climbing Stairs
1. 예시 문제 : House Robber (링크 : 풀이)
2. 예시 문제 : Longest Increasing Subsequence (링크 : 풀이)