여러 개의 하위문제를 푼 후, 그 결과를 쌓아올려 주어진 문제를 해결하는 알고리즘이다.
DP로 풀 수 있는 문제는 다음과 같은 조건을 만족시킨다.
1. 최적 부분 구조
- dp[1, 2, .... i-1]로 dp[i]가 도출될 수 있어야 한다.
2. 겹치는 부분 문제
- 문제를 작은 문제로 쪼갤 수 있어야 한다.
- 큰 문제와 작은 문제를 같은 방법으로 풀 수 있어야 한다.
분명 한글로 쓰고 있는데 뭔 소린지 모르겠다.
진정하고 fibo(5)
를 트리 구조로 그리면서 이해해보자.
int fibo(int n) {
if(n<=1) return 1;
return fibo(n-2)+fibo(n-1);
}
아! 그려보니까 감이 좀 잡힌다.
피보나치 수열을 재귀호출로 풀면, fibo(2)
를 계속 호출해야 한다는 '겹치는 부분문제'가 계속 생기니까 커다란 낭비가 생긴다.
그래서 기억이 날락말락한 게, 자료구조 수업시간에 교수님께서 피보나치 수열을 코드로 짤 때 recursion VS iteration 이슈에서 recursion으로 풀면 굉장히 비효율적이기 때문에 iteration으로 푸는 게 낫다라는 말씀을 잠결에 들은 것 같다.
그렇다면 피보나치 수열을 좀 더 똑똑하게 풀 수 있는 방법은 없을까?
딱 봐도 fibo(2)
을 낭비 없이, 한 번만 호출하고도 풀 수 있는 방법은 해당 값(fibo(2)
)을 기억+저장(메모이제이션, 캐싱)하면서 푸는 것이 있을 것이다.
피보나치 수열을 DP로 풀었을 때와 아니었을 때의 함수 호출횟수를 비교하기 위한 기초적인 예제를 풀어보자.
//동적 계획법의 기초 문제
#include <iostream>
using namespace std;
int n, cnt1=0, cnt2=0;
int dp[44]; //#1 테이블 정의하기: dp는 n항의 값을 저장하는 배열
//1. 재귀 호출 함수
int recur_fib(int n) {
if(n==1 || n==2) {
cnt1++; return 1;
}
return recur_fib(n-1)+recur_fib(n-2);
}
// 2. dp로 풀기
int dp_fib(int n) {
dp[1] = dp[2] = 1; //#3. 초기값 셋팅
for(int i = 3; i <= n; i++) {
dp[i] = dp[i-2]+dp[i-1]; //#2. 점화식 찾기
cnt2++;
}
return dp[n];
}
int main(){
ios_base::sync_with_stdio(false); cin.tie(0);
cin >> n;
recur_fib(n);
dp_fib(n);
cout << cnt1 << " " << cnt2;
}
- STEP 1. 상태 정의 하기
: dp배열이 무엇인지 정의하여야 한다.제일 중요한 과정이자 내가 제일 못하는 과정.- STEP 2. 점화식 찾기
- STEP 3. 초기값 설정하기
문제를 직접 풀면서 이해하면 빠를 것이다.
문제 풀러 바로가기 >> 9095 :: 1,2,3 더하기
정수 4를 1, 2, 3의 합으로 나타내는 방법은 총 7가지가 있다. 합을 나타낼 때는 수를 1개 이상 사용해야 한다.
정수 n이 주어졌을 때, n을 1, 2, 3의 합으로 나타내는 방법의 수
를 구하는 프로그램을 작성하시오.
첫째 줄에 테스트 케이스의 개수 T가 주어진다. 각 테스트 케이스는 한 줄로 이루어져 있고, 정수 n이 주어진다. n은 양수이며 11보다 작다.
각 테스트 케이스마다, n을 1, 2, 3의 합으로 나타내는 방법의 수를 출력한다.
☝️ STEP 1. 테이블을 정의한다.
DP[i] = i를 1, 2, 3의 합으로 나타내는 방법의 수
✌️ STEP 2. 점화식을 찾는다.
나 같은 비에이비오(babo)는 차근차근 해야 하므로 하나하나 다 적어보도록 하겠다.
DP[1]
은 {1}로 나타낼 수 있으므로, DP[1]=1
이다.
DP[2]
은 {1+1, 2}로 나타낼 수 있으므로, DP[2]=2
이다.
DP[3]
은 {1+1+1, 2+1, 1+2, 3}으로 나타낼 수 있으므로, DP[3]=4
이다.
DP[4]
은 {1+1+1+1, 2+1+1, 3+1, 1+2+1, 1+1+2, 2+2, 1+3} 으로 나타낼 수 있으므로, DP[4]=7
이다.
여기서 잠깐! DP[4]
를 구하면서 이상한 찝찝함+기시감이 든다. 뭔가 똑같은 노동을(?) 하고 있는 기분이...😣
그러면 잠깐 멈추고, 한번 DP[1], DP[2], DP[3], DP[4]를 구했던 과정을 바탕으로
수식을 분석하여 점화식을 도출해보자!
DP[i] = DP[i-1]+DP[i-2]+DP[i-3] (단, i>=4)
👌 STEP 3. 초기값을 셋팅한다.
점화식을 찾았으면, 초기값 셋팅은 비교적 쉽다.
아까 이끌어낸 점화식의 조건에 i>=4가 붙었으므로, i<4인 경우를 정의해주면 된다.
DP[1] = 1;
DP[2] = 2;
DP[3] = 4;
#include <iostream>
using namespace std;
int t;
int main() {
ios_base::sync_with_stdio(false); cin.tie(0);
cin >> t;
int DP[12];
DP[1] = 1; DP[2] = 2; DP[3] = 4; //초기값 셋팅
for(int i = 4; i < 12; i++) {
DP[i] = DP[i-1] + DP[i-2] + DP[i-3];
}
while(t--){
int n;
cin >> n;
cout << DP[n] << "\n";
}
}