개념

동적 계획법(DP)은 부분 문제의 답을 저장해 두고 다시 필요할 때 사용하는 방법입니다. 피보나치 수열을 통해 같은 값을 반복해서 계산하는 재귀 풀이를 어떻게 바꿀 수 있는지 살펴봅니다.

피보나치 수열과 상태

피보나치 수열은 F0=0F_0=0, F1=1F_1=1로 시작하며, 이후의 항은 바로 앞의 두 항을 더한 값입니다.

Fi=Fi−1+Fi−2(i≥2).F_i=F_{i-1}+F_{i-2}\quad(i\ge2).

i0123456
FiF_i0112358

DP에서 상태는 저장할 값이 어떤 문제의 답인지를 나타냅니다. 여기서는 DiD_i를 피보나치 수열의 ii번째 항으로 정의합니다. 그러면 초기값은 D0=0D_0=0, D1=1D_1=1이고, 이전 상태로 다음 상태를 구하는 전이식은 Di=Di−1+Di−2D_i=D_{i-1}+D_{i-2}입니다.

단순 재귀에서 반복되는 계산

정의를 그대로 재귀에 적용하면 F0F_0과 F1F_1은 즉시 반환하고, 나머지는 앞의 두 항을 재귀로 구해 더합니다. F5F_5를 계산하는 첫 호출은 F4F_4와 F3F_3으로 나뉩니다.

호출값을 구하기 위해 호출하는 항
F5F_5F4F_4, F3F_3
F4F_4F3F_3, F2F_2
F3F_3F2F_2, F1F_1
F2F_2F1F_1, F0F_0

F5F_5에서 직접 호출한 F3F_3과 F4F_4를 구할 때 호출한 F3F_3은 같은 계산입니다. 두 호출은 각각 F2F_2와 F1F_1을 구하고, 그 안에서도 같은 계산을 반복합니다. 실제로 F5F_5를 구할 때 F3F_3은 22번, F2F_2는 33번 호출됩니다. 필요한 항은 F0F_0부터 F5F_5까지 66개이지만, 함수 호출은 종료 조건에 도달하는 호출까지 합쳐 1515번 발생합니다.

메모이제이션: 계산한 항을 재사용

Top-down DP는 재귀 호출에서 구한 결과를 저장하는 방식입니다. 먼저 해당 항이 저장되어 있는지 확인하고, 저장되어 있다면 그 값을 반환합니다. 처음 구하는 항이면 앞의 두 항을 구해 더한 뒤 저장합니다. 계산 결과를 기억해 두는 것을 메모이제이션이라고 합니다.

F5F_5를 구하며 F4F_4 안에서 F3=2F_3=2를 계산해 저장했다면, 이후 F5F_5가 다시 F3F_3을 요청할 때는 저장된 22를 반환합니다. 그 아래의 F2F_2와 F1F_1 계산도 다시 수행할 필요가 없습니다. 각 항의 덧셈은 한 번씩만 수행합니다.

계산 전 상태를 −1-1로 표시하면 실제 결과인 F0=0F_0=0과 구별할 수 있습니다. 재귀 종료 조건인 F0F_0과 F1F_1은 미리 저장합니다.

반복문: 작은 항부터 계산

Bottom-up DP는 필요한 작은 상태부터 순서대로 채우는 방식입니다. D0D_0과 D1D_1을 먼저 저장하고 i=2i=2부터 NN까지 증가시키며 DiD_i를 계산합니다.

D2=1+0=1,D3=1+1=2,D4=2+1=3,D5=3+2=5.D_2=1+0=1,\quad D_3=1+1=2,\quad D_4=2+1=3,\quad D_5=3+2=5.

DiD_i를 구할 때 참조하는 Di−1D_{i-1}과 Di−2D_{i-2}는 이미 계산되어 있습니다. 계산 순서를 직접 정하므로 재귀 호출이 필요하지 않습니다. FNF_N만 필요하면 직전 두 항만 보관하여 저장 공간을 줄일 수도 있습니다.

복잡도 비교

구하려는 항의 번호를 NN이라고 합니다. 단순 재귀는 한 호출이 최대 두 호출로 나뉘므로 시간복잡도의 상한을 O(2N)O(2^N)으로 잡을 수 있습니다. 동시에 쌓이는 재귀 호출의 깊이는 O(N)O(N)입니다.

방법시간복잡도공간복잡도
단순 재귀O(2N)O(2^N) 상한호출 스택 O(N)O(N)
메모이제이션O(N)O(N)저장 배열·호출 스택 O(N)O(N)
배열을 채우는 반복문O(N)O(N)저장 배열 O(N)O(N)
직전 두 항만 저장하는 반복문O(N)O(N)O(1)O(1)

DP의 시간은 상태 수와 각 상태를 계산하는 데 필요한 연산 수로 구합니다. 피보나치 수열은 상태가 N+1N+1개이고, 초기값 이후의 상태마다 덧셈 한 번을 수행하므로 O(N)O(N)입니다.

예제

문제

피보나치 수열은 F0=0F_0=0, F1=1F_1=1이며, i≥2i\ge2일 때 Fi=Fi−1+Fi−2F_i=F_{i-1}+F_{i-2}로 정의된다. FNF_N을 구하여라.

입력

첫째 줄에 음이 아닌 정수 NN이 주어진다. 0≤N≤200\le N\le20이다.

출력

FNF_N을 출력한다.

예제 입력

5

예제 출력

5

예제 코드

C++20

#include <iostream>

using namespace std;

int dp[21];

int main() {
    int n, i;

    cin >> n;

    // 피보나치 수열의 초기값
    dp[0] = 0;
    dp[1] = 1;
    for (i = 2; i <= n; ++i) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }

    cout << dp[n] << '\n';
}