
알고리즘 문제 풀이 스터디를 하며 만든 자료입니다.
개인 공부 기록용으로 올립니다.
Dynamic Programming(동적 계획법)은 하나의 큰 문제를 여러 개의 작은 문제로 나누어서, 그 결과를 저장하여 다시 큰 문제를 해결할 때 사용하는 문제해결 패러다임입니다.
완전탐색, DFS, BFS로 해결할 수 있지만 경우의 수가 너무 많아 속도가 느려지는 문제들을 효율적으로 해결하기 위해 등장했습니다.
💡 한 교수는 DP를 "기억하기 알고리즘"이라고 번역하기도 했습니다.
최적 부분 구조 (Optimal Substructure)
중복 부분 문제 (Overlapping Subproblems)
DFS/BFS로 풀 수 있지만 경우의 수가 너무 많은 문제
중복적인 연산이 많이 발생하는 경우
예시: 가장 빨리 도착하는 경로의 소요 시간, 주식 매매 최적 타이밍
"어떻게 해야 뒤로 돌아가지 않을 수 있을까?"

| Top-Down | Bottom-Up | |
|---|---|---|
| 구조 | recursive(재귀) | iterative(반복문) |
| subproblem 결과 저장 | memoization | tabulation |
| 선호하는 경우 | subproblem의 일부만 계산해도 최종 솔루션을 구할 수 있을 때 | 모든 subproblem을 계산해야 최종 솔루션을 구할 수 있을때 |
큰 문제를 해결하기 위해, 재귀적으로 작은 문제를 호출하며 해결
→ 큰 문제에서 작은 문제로(top-down) 내려가며 해결하는 방식
⚠️ 주의사항: 함수 호출이 많아지면 호출 오버헤드, 스택 오버플로우 위험 있음
작은 규모의 문제부터 시작해 전체 큰 문제를 해결
→ 작은 문제를 해결하면서 점점 위로(bottom-up) 올라가는 방식 (주로 사용)
💡 장점: 스택 오버플로우 걱정 없음, 경우에 따라 공간 최적화 가능
ex. 직전 값만 사용하는 경우 리스트가 아닌 변수 사용
근본적인 개념으로 "결과값을 기억하고 재활용" 한다는 측면에서는 Memoization, Tabulation이 크게 다르지 않음
정상까지 오르는데 N번의 스텝이 필요한 계단에서 한번에 한 스텝 혹은 두 스텝을 오를 수 있을때 정상까지 올라가는 방법의 수는?
climb(n) = climb(n-1) + climb(n-2)// 시간복잡도: O(2^n) - 매우 비효율적!
function climb(n){
if(n<=2) return n;
return climb(n-1) + climb(n-2)
}

문제점 : 동일한 input에 대한 function call이 많은 것을 확인 할 수 있음
→ 메모해 뒀다가 이후 재사용하는 방식으로 변경
// 시간복잡도: O(n)
let memo = [];
function climb(n){
if(n<=2) return n;
// 이미 계산된 값이 있으면 바로 반환
if (memo[n] !== undefined)
return memo[n];
// 없으면 계산 후 저장
memo[n] = climb(n-1) + climb(n-2);
return memo[n]
}

Top-Down of DP
n개 정수 수열에서 연속된 몇 개의 수를 선택해서 구할 수 있는 합 중 가장 큰 값은?
예시: [10, -4, 3, 1, 5, 6, -35, 12, 21, -1] -> 33(12+21)
arr[i] 까지 왔을때, 최대인 연속 합이 나오는 2가지
1. arr[i] 자체만 선택
2. 이전까지의 최대 연속합에 arr[i] 를 더하는 경우
dp[i] = Math.max(arr[i], dp[i-1] + arr[i])// 시간복잡도: O(n), 공간복잡도: O(n)
function solution(N, arr) {
const dp = Array(N).fill(0);
dp[0] = arr[0];
let maxSum = dp[0];
for (let i = 1; i < N; i++) {
dp[i] = Math.max(arr[i], dp[i - 1] + arr[i]);
maxSum = Math.max(maxSum, dp[i]);
}
return maxSum;
}

잘 보면 결국 바로 직전의 값만 사용하는 것을 확인할 수 있음
→ 꼭 배열 형태로 사용할 필요가 없다!
dp 배열 없이 현재 상태만 저장해서 메모리 사용을 줄임
// 시간복잡도: O(n), 공간복잡도: O(1)
function solution2(N, arr) {
let current = arr[0];
let maxSum = arr[0];
for (let i = 1; i < N; i++) {
current = Math.max(arr[i], current + arr[i]);
maxSum = Math.max(maxSum, current);
}
return maxSum;
}
