본문 바로가기

2026 코드트리 청약 챌린지

[코드트리 후기] 6주차, Dynamic Programming 개념 정리

 

이번 블로그에서는 최근에 코드트리에서 공부했던 Dynamic Programming 기법을 쭉 정리해보려고 한다.

 


Memoization의 힘

피보나치 수열을 출력하는 프로그램을 재귀 함수만으로 한번 만들어보자.

 

 

프로그래밍을 열심히 공부했다면, 이 정도는 어렵지 않게 구현할 수 있다.

그런데 한가지 문제가 있다. 위 프로그램에 50을 집어넣으면 너무 오래 걸려서 결과가 출력되질 않는다.

 

각 fibonacci 함수는 내부에서 fibonacci 함수를 2회 호출한다. 그럼 내부에서 실행된 fibonacci 함수는 또다시 fibonacci 함수를 2회 호출하고, 그 과정이 계속해서 반복된다. n이 2 이하가 될 때까지 n을 1씩 빼면서 재귀적으로 호출할 테니, 함수의 전체 깊이는 n - 1이 된다.

 

그림으로 그려보면 훨씬 직관적으로 와닿는다.

 

 

따라서 이 코드는 O(2^n)의 시간 복잡도를 갖는다고 볼 수 있다. 코드트리의 시간복잡도 챕터를 공부했다면, 이는 말도 안되는 시간복잡도임을 바로 알 수 있다. n이 30 정도만 되어도 2^30 = 1,073,741,824으로 fibonacci 함수의 호출 횟수가 10억회를 돌파한다! 

 

그런데 그림을 그려보면 직관적으로 보이는 것이 하나 있다. 이 그림에는 반복되는 노드가 너무 많다. 예를 들어서, F(3)은 이 그림에서 3번 등장했다.

 

 

그리고 곰곰히 생각해 보면, 이 함수가 이렇게 끔찍한 시간복잡도를 갖는 원인은 이렇게 중복되는 노드를 계속해서 계산하기 때문이다. N이 커질수록 F(3)은 2^n에 비례하는 횟수만큼 실행될 것이다.

 

그렇다면 F(3)을 이미 한번 계산했으니, F(3)의 결과를 저장해놓고 F(3)이 필요할 때마다 그냥 저장된 결과를 반환시킬 수 있지 않을까?

 

이것이 바로 Memoization이다. 프로그램의 실행 과정에서 프로그램이 동일한 문제를 다시 마주쳤을 때, 문제를 다시 계산하지 않고 이미 저장된 결과를 그대로 반환하여 중복되는 문제를 O(1)의 시간에 내놓는다. 백엔드 공부를 좀 해봤다면 마주쳤을 캐싱 기법과 동일한 원리다.

 

이제 이 아이디어를 fibonacci 함수에 적용해보자. 코드를 아래와 같이 개선할 수 있다.

 

 

이제 코드를 실행해보면 큰 수를 입력해도 엄청 빠르게 계산된다! 아까의 그림을 위 코드 구조를 반영하여 다시 한번 살펴보자. 

 

 

 

반복되는 노드구조가 사라지고 전체적으로 아주 깔끔하게 변한 걸 볼 수 있다. 아까는 총 15회의 함수 호출이 발생했지만 이젠 9회의 함수 호출로 줄어들었다. n이 더 크다면 그 수치는 훨씬 드라마틱하게 변한다.

 

Memoization 덕분에 이제 위 코드는 O(n)이라는 적은 시간 복잡도를 자랑한다.

 

 


문제를 중복되는 문제로 작게 쪼개기

Memoization은 강력하지만, Memoization을 쓰기 위해 필요한 조건은 "문제를 중복되는 문제로 쪼개는 것"이다.예시를 통해서 문제를 어떻게 쪼개는지 살펴보자.

 

코드트리 문제 링크

 

 

만약 1, 2, 5 이렇게 3종류의 동전이 주어졌고, 11을 동전 갯수를 최소로 하여 만들어야 한다고 할 때, 점화식을 어떻게 만들 수 있을까?

 

F(n)을 n을 만들 수 있는 최소 동전의 갯수를 반환하는 함수라고 해보자. 그러면

 

  • 1원을 넣는 케이스: F(10) + 1
  • 2원을 넣는 케이스: F(9) + 1
  • 5원을 넣는 케이스: F(6) + 1

이 세가지 값 중 최소가 되는 값을 선택할 수 있을 것이다. 그 값이 F(11)이 될 수 있을 것이라고 유추해 볼 수 있다.

 

이를 점화식으로 나타내면 아래와 같다.

 

이것도 아까 피보나치 함수처럼 아래와 같은 재귀 트리를 만들 수 있다. 중간 중간에 반복되는 노드가 있으니 Memoization을 적용할 수 있다는 사실을 쉽게 확인할 수 있다.

 

 

참고로 여기서 F(10), F(9), F(6)과 같이 F(11)을 푸는데 사용된 쪼개진 문제들을 Subproblem이라고 한다.

 

위 문제를 코드로 풀어보면 아래와 같다.

 

 

 

 


Top-down과 Bottom-up 방식

지금까지는 재귀 함수를 이용한 Memoization 방식을 살펴봤다. 그런데 위 점화식을 다시 곰곰이 생각해 보면, 굳이 재귀를 사용해서 n부터 내려갈 필요 없이, 반대로 0에서 n으로 올라가는 방식을 생각해 볼 수 있다.

 

다시 1, 2, 5의 동전으로 11을 만드는 케이스를 생각해보자.

 

F(0)은 초기 값 0로 고정되어 있으니 F(1)부터 시작한다.

 

 

F(1)은 F(1 - 1) + 1, F(1 - 2) + 1, F(1 - 3) + 1 셋 중 가장 작은 값을 고를 수 있는데, F(0) + 1을 제외하면 나머지는 범위를 벗어나므로 F(1)을 1로 채워줄 수 있다.

 

 

F(2)는 F(2 - 1) + 1, F(2 - 2) + 1, F(2 - 3) + 1 셋 중 가장 작은 값을 고를 수 있고, F(1) + 1, F(0) + 1만 가능하다. 둘 중 작은 값은 F(0) + 1이므로 다시 1로 채워준다.

 

이런 식으로 이 과정을 F(11)까지 반복하면, F(11)에 아까와 동일하게 최소 동전 개수가 담기게 된다. 이제 F(11)을 반환하면 된다.

 

코드로 나타내면 아래와 같다.

 

 

 

재귀 함수처럼 n에서 내려가는 방식을 Top-down 방식이라고 하고, 지금의 배열 방식처럼 n까지 채워나가는 것을 Bottom-up 방식이라고 한다. (Bottom-up 방식은 표를 채워나간다고 해서 Tabulation이라고 부르기도 한다.)

 

보통은 DP 문제를 풀 때는 Bottom-up 방식이 선호되는 편인데, 가끔 Top-down 방식이 필요한 경우가 있으므로 상황에 따라 둘 다 구현할 수 있도록 연습해 두는 게 좋다.

 

 


Dynamic Programming

이렇게 해서 Memoization과 Subproblem으로 쪼개는 방법에 대해서 알아보았다. 문제를 더 작은 Subproblem으로 쪼개고, 중복되는 Subproblem들을 Memoization을 활용하여 쳐내어 문제를 푸는 기법을 Dynamic Programming (동적 계획법)이라고 한다.

 

DP는 정말 많은 문제에서 활용할 수 있다. 최근에 봤던 정말 독특했던 DP 풀이는 n개의 올바른 괄호를 가진 문자열을 모두 출력하는 문제를 DP로 풀어낸 풀이였다. (문자열의 개수를 출력하는 게 아니라 모든 가능한 문자열을 출력하는 것이다!)

 

하지만 DP는 중요한 조건이 몇 개 있다.

 

  • 중복되는 Subproblem이 존재해야 한다.
  • Subproblem의 최적 해들로 Problem의 최적 해를 도출할 수 있어야 한다.

왜 중요한 조건인지 하나씩 살펴보자.

 

 


중복되는 Subproblem이 존재해야 한다

좋은 예시로 merge sort를 살펴보자. merge sort는 하나의 큰 배열을 계속해서 반으로 쪼개서 풀어내니까, merge sort도 하나의 큰 Problem을 작은 Subproblem을 이용해 풀어내는 과정이 담겨있다. 하지만 merge sort의 풀이법을 DP라고 부르지 않는다. 그 이유는 Subproblem이 중복되지 않기 때문이다.

 

merge sort는 이미 있는 배열을 반으로 쪼개고, 또 반으로 쪼개진 배열을 다시 반으로 쪼갠다. 이 과정을 계속 거쳐서 배열의 원소가 1개가 될 때까지 쪼개고, 그때부터 배열을 정렬해서 합쳐나간다. 문제는 배열을 쪼개는 과정에서 "이미 subarray를 정렬했었던 케이스"가 전혀 존재하지 않는다. merge sort 함수를 재귀적으로 호출할 때 아직 한 번도 정렬하지 않은 배열만 계속해서 던져준다.

 

DP의 핵심은 중복되는 subproblem을 memoization을 사용하여 이미 계산된 부분을 쳐내는 것에 있다. merge sort는 problem을 subproblem으로 쪼갤 수는 있지만 subproblem이 중복되지 않으므로 memoization을 사용할 수 없고, 따라서 DP라고 할 수 없다.

 

참고로 이렇게 problem을 subproblem으로 쪼개었지만 subproblem이 중복되지 않는 경우는 분할 정복(Divide and conquer)이라고 부른다.

 

 

 


Subproblem이 Problem을 해결해야 한다

아래 문제를 살펴보자.

 

코드트리 문제 링크

 

 

위 문제의 풀이 방법을 한번 생각해 보자. 내가 처음에 생각해 본 아이디어는 아래와 같다.

 

 

간단하게 설명하자면, DP[x][y]를 (x, y) 칸에서 max-min 값을 최소로 하는 경로의 (min, max) 쌍으로 정의했다.

 

그리고 (x - 1, y)에서 (x, y)로 이어지는 경로 a와 (x, y - 1)에서 (x, y)로 이어지는 경로 b가 있을 때, a의 max-min 값과 b의 max-min 값을 비교하고 둘 중 최소가 되는 경로를 선택하여 DP[x][y]에 집어넣는다.

 

뭔가 괜찮은 것 같고 예시도 통과하지만, 아래와 같은 반례가 있다.

 

두 케이스 모두 문제는 (3,3) 지점에 도달하면서 시작된다.

(3, 3)에서 (3, 2) 혹은 (2, 3) 중에서 하나를 골라야 하는데, 둘 다 max - min의 값이 2로 동일하다. 이때 코드가 왼쪽 경로 min 5, max 7을 골랐다면 왼쪽 케이스에서 잘못된 답을, 위쪽 경로 min 3, max 5를 골랐다면 오른쪽 케이스에서 잘못된 답을 반환한다.

 

그러므로 이 풀이 방법은 Subproblem의 최적해가 상위 Problem의 최적해를 구할 수 없으므로 올바른 DP 풀이로 볼 수 없다.

 

그래서 다시 한번 머리를 굴려보면, 이 문제에서 필요한 상태값은 현재 좌표, 경로의 min값, 경로의 max값 이렇게 3가지다. 만약 처음부터 min 값을 5로 제한했다면 어떨까? 즉, 경로를 탐색할 때 무조건 5보다 큰 칸만 밟을 수 있다고 조건을 걸고 탐색하는 것이다. 그럼 탐색할때 지금까지 지나온 경로의 min max 값을 복잡하게 고려할 것 없이, 탐색할 때 무조건 5보다 큰 칸 중에 가장 작은 칸만 계속 밟으며 최댓값을 최소로 유지하면 된다. 이러면 오른쪽 케이스에서 최적의 해를 찾을 수 있다. 반대로, min 값을 1로 제한했다면 마찬가지로 무조건 작은 수만 밟으며 경로를 탐색하는데, 이 때는 왼쪽 케이스에서 최적의 해를 찾을 수 있다.

 

이렇게 min 값을 고정한다는 아이디어를 확장할 수 있다. 칸에 들어갈 수 있는 모든 숫자들을 한 번씩 min 값으로 고정하고 모든 타일을 순회하는 것이다. 마침 문제의 조건에도 모든 칸에는 1~100 사이의 정수만 들어갈 수 있다고 했으니 TLE가 발생할 우려도 없다.

 

이는 DP 배열에 하나의 상태를 추가하고 3차원으로 확장하여 정의할 수 있다. DP[x][y][min] 를 "min보다 큰 숫자만을 사용하여 x, y 타일에 도달하는 경로 중 타일의 최댓값이 가장 최소가 되는 값"으로 정의한다. 그리고 이제 DP[n][n][min]의 값을 갱신할 때마다 answer 변수에 max - min의 최소 값을 기록하면 된다. 그러면 이제 Subproblem이 Problem의 최적해를 올바르게 구하는 데 사용할 수 있다.

 

이처럼 과거에 골랐던 선택이 미래에 최적의 선택을 하는데 영향을 끼친다면, DP 배열에 상태가 빠졌다는 신호일 수 있다. 점화식을 세우기 전에 문제에서 어떤 상태들이 있는지 미리 정리해 보는 습관을 들여두면 좋다.

 

마치며

사실 나는 DP가 아직도 조금 까다롭다고 생각한다. 간단한 DP 문제는 여러 번 풀어봐서 괜찮을지 몰라도 DP 문제에는 정말 다양한 유형이 있고, 처음 보는 유형은 점화식을 어떻게 세워야 할지 감이 안 잡힐 때도 많다.

 

하지만 코딩테스트에서 만나는 DP는 결국 이미 봤었던 문제를 조금만 비틀어서 내는 경우가 대부분이다. 코드트리에서 다양한 유형의 문제를 꾸준히 풀어나가며 점점 DP를 정복해 나가면 당당하게 풀어낼 수 있는 자신감을 얻을 수 있을 것이다.


▼ 코드트리 청약 통장 챌린지 참여하기
https://www.codetree.ai/ko/no-free-lunch-2026/?ref=NKJVVB

#코드트리 #코딩테스트 #코테공부 #DP #다이나믹프로그래밍