하나의 큰 문제를 여러 개의 작은 문제로 나누어서 그 결과를 저장, 다시 큰 문제를 해결할 때 저장된 결과를 사용하는 방법. 즉, 답을 재활용한다고 생각하면된다.
메모리를 적절히 사용해 수행 시간 효율성을 비약적으로 향상 시키는 방법이다.
대표적인 DP 문제 : ⭐️배낭문제⭐️, 1로 만들기, 화폐 구성, 피보나치 수열 등
최적 부분 구조 (Optimal Substructure)
: 큰 문제를 작은 문제로 나눌 수 있으며, 작은 문제의 답을 모아서 큰 문제를 해결할 수 있는 경우.
중복되는 부분 문제 (Overlapping Subproblem)
: 동일한 작은 문제가 반복적으로 발생하는 경우.
DP 문제에서 제일 유명한 피보나치 수열을 예시로 들었다.
피보나치 수열은 아래 예시와 같이 첫 번째 항과 두 번째 항이 1이며 그 뒤의 항은 바로 앞 두 항의 합인 수열이다. 편의상 0을 0번째 항으로 놓기도한다. 이 수열을 이루는 숫자를 피보나치 수라고 부른다.
(0), 1, 1, 2, 3, 5, 8, 13, 21, 34, .....
피보나치 수열을 재귀적으로 구하는 함수는 아래와 같다.
public int fibo (int n) {
if (n < 1)
return 0;
// 첫 번째 항이거나 두 번째 항이면 무조건 1이기 때문에 1을 반환해준다.
if (n == 1 || n == 2)
return 1;
return fibo(n - 1) + fibo(n - 2);
}
문제는 이 메소드를 호출하게 될 경우 아래 그림처럼 매우 많은 횟수의 호출과 중복이 일어나게 된다.
이런 경우 재귀적으로 문제를 풀 필요없이 값을 더해 나가며 앞의 부분 문제의 결과를 다음 문제의 답을 구하는데 활용이 가능하다. 즉, DP를 이용해 편리하게 구할 수 있는 것이다.
탑 다운 (하향식) : 큰 문제를 해결하기 위해 작은 문제를 재귀적으로 호출, 작은 문제들이 해결됐을때 큰 문제에 대한 답을 얻을 수 있도록 구현하는 방식.
import java.util.*;
public class Main{
// 탑다운 방식 중 메모이제이션 피보나치 수열 코드 예시
public static long[] d = new long[100];
public static void fibo(x){
System.out.println("f("+x+")"+" ");
if(x==1 || x==2)
return 1;
if(d[x] != 0)
return d[x];
d[x] = fibo(x-1) + fibo(x-2);
return d[x];
}
public static void main(String[] args){
// 첫 번째 피보나치 수, 두 번째 피보나치 수 = 1
d[1] = 1;
d[2] = 1;
int n = 50; // 50번째 피보나치 수 계산
System.out.println(fibo(n));
}
바텀 업 (상향식) : 아래쪽부터 작은 문제를 해결, 먼저 계산된 문제들의 값을 활용해 그 다음의 문제까지 차례로 해결하는 방식. 반복문 활용
DP의 전형적인 형태 = 바텀 업 방식
결과 저장용 배열 or 리스트 = DP 테이블 이라고 부른다.
import java.util.*;
public class Main{
// 바텀 업 방식 피보나치 수열 구현 예시
public static long[] d = new long[100];
public static void main(String[] args){
// 첫 번째 피보나치 수, 두 번째 피보나치 수 = 1
d[1] = 1;
d[2] = 1;
int n = 50; // 50번째 피보나치 수 계산
//피보나치 함수 반복문으로 구현(보텀업 DP)
for(int i=3;i<=n;i++){
d[i] = d[i-1] + d[i-2];
}
System.out.println(d[n]);
}
동적 계획법과 분할 정복의 경우 큰 문제를 작은 부분 문제로 나누어서 풀어낸다는 점에서 유사한 모습을 보이지만, 차이점도 존재한다.