|
| 1 | +--- |
| 2 | +description: |
| 3 | +aliases: |
| 4 | + - DP |
| 5 | +created: 2025-05-18 |
| 6 | +modified: 2025-10-12 |
| 7 | +tags: |
| 8 | + - review |
| 9 | +references: '["https":"//github.com/crapas/dp",]' |
| 10 | +--- |
| 11 | + |
| 12 | +- DP 문제 풀이 조건 |
| 13 | + - **최적 부분 구조, 최적 하위 구조 (Optimal Substructure)** |
| 14 | + - *크기가 n인 문제*에서, 문제 해결 형태는 같지만 *n 미만의* 원소를 가지는, 더 작은 크기의 문제의 풀이법을 사용하는 것이 최적의 풀이법에 해당하면 이를 optimal substructure |
| 15 | + - *크기가 n인 문제*에서 *`k < n`인 k의* 비슷한 문제의 관점에서 정의한다는 뜻, 더 작은 원소에 대한 최적의 풀이법을 찾고, 이들을 결합해서 최종 풀이법 완성 |
| 16 | + - 문제를 풀기 위한 최적의 해가, 그 하위 문제들의 최적해를 이용해 구성될 수 있을 때 |
| 17 | + - 피보나치 |
| 18 | + - `F(n) = F(n-1) + F(n-2)` |
| 19 | + - 하위 문제의 답을 조합하여 상위 문제의 답이 됨 |
| 20 | + - **중복 부분 문제, 하위 문제의 반복 계산 (Overlapping Subproblems)** |
| 21 | + - 큰 문제를 풀기 위해 동일한 작은 문제를 반복해서 계산해야 할 때 |
| 22 | + - 작은 문제를 단 한번만 풀자 |
| 23 | + - 메모이제이션, 타뷸레이션 |
| 24 | + - 피보나치 |
| 25 | + - `F(5) → F(4) + F(3)` |
| 26 | + - `F(4) → F(3) + F(2)` |
| 27 | + - `F(3) → F(2) + F(1)` |
| 28 | + - 중복되는 연산이 있는 경우 |
| 29 | +- DP 적용 체크 리스트 |
| 30 | + - 문제를 같은 형태의 하위 문제로 나눌 수 있는가 |
| 31 | + - 하위 문제들의 해결로 상위 문제를 해결할 수 있는가 |
| 32 | + - 하위 문제의 계산이 반복되는가 |
| 33 | + - 최적화, 최대화, 최소화나 어떤 작업의 경우의 수를 구하는 유형의 문제인가 |
| 34 | +- 풀이 순서 |
| 35 | + 1. DP 적용할 수 있는지 확인 |
| 36 | + 2. 점화식 또는 재귀 과정 정의 |
| 37 | + 1. 문제를 하위 문제를 사용해 하향식으로 정의 |
| 38 | + 2. 맨 아래에 해당하는 '기본 경우'에 대한 답을 정의 |
| 39 | + 3. 종료 조건 추가 |
| 40 | + 3. 메모 전략을 시도 |
| 41 | + 4. 상향식으로 문제 풀이에 도전 |
| 42 | +- 방법 |
| 43 | + - 하향식 |
| 44 | + - 메모이제이션 (캐쉬 기법) |
| 45 | + - 누적 (이전 계산 재사용) |
| 46 | + - 재귀로 접근 |
| 47 | + - 상향식 |
| 48 | + - 타뷸레이션 |
| 49 | + - 점화식 필요 |
| 50 | + - 반복문으로 접근 |
| 51 | +- DAG에 적용 가능 (배열, 트리, 위상 정렬) |
| 52 | +- 상향식 DP가 좋지 않은 경우 |
| 53 | + - combination (파스칼 삼각형) |
| 54 | + - $C(n,\;m) = C(n - 1,\;m) + C(n - 1,\;m - 1)$ |
| 55 | + - ![[image-DP.png]] |
| 56 | + - 재귀는 목표한 값만 찾지만, 상향식 DP는 모든 파스칼 삼각형을 찾는다 |
| 57 | + - ![[image-DP-1.png]] |
| 58 | +## 피보나치 |
| 59 | +- 하향식 |
| 60 | + - recursive |
| 61 | + - `dp(n - 1) + dp(n - 2)` |
| 62 | + - $O(2^{n})$ |
| 63 | + - 메모이제이션 |
| 64 | + - `dp_cache[n]`에 구한 값 저장 |
| 65 | + - $O(n)$ |
| 66 | + - 스택사용으로 살짝 성능 저하 |
| 67 | + - 캐쉬 사용으로 공간 복잡도 O(n) |
| 68 | +- 상향식 |
| 69 | + - $dp[1] = 1$ |
| 70 | + - $dp[2] = 1$ |
| 71 | + - $dp[N] = \min ( dp[i - 1] + dp[i - 2]) \quad \text{for } i = 0, 1, \dots, N )$ |
| 72 | + - $O(N)$ |
| 73 | +- 선형수학 |
| 74 | + - 행렬로 계산시 $O(log(n))$ |
| 75 | +- [[다이내믹 프로그래밍 완전 정복]] p.85 |
| 76 | +## 역 사이 최소 비용 구하기 |
| 77 | +- $minCost[N] = \min ( minCost[i] + cost[i][N] \quad \text{for } i = 0, 1, \dots, N )$ |
| 78 | +- 상향식으로 최소 비용 갱신 |
| 79 | +- [[다이내믹 프로그래밍 완전 정복]] p.87 |
| 80 | + |
| 81 | +## 부분 문자열 다루기 |
| 82 | +- 숫자로 이루어진 문자열에서, 부분 문자열 중 앞의 절반과 뒤의 절반 숫자의 합이 같은 부분 문자열 중, 가장 긴 부분 문자열의 길이 |
| 83 | +- $k = (i + j) / 2 \quad \text{mid value}$ |
| 84 | +- $S(i,\; j) = S(i,\;k) + S(k + 1,\; j)$ |
| 85 | +- [[다이내믹 프로그래밍 완전 정복]] p.89 |
| 86 | +## 행렬에서 최소 이동 비용 |
| 87 | +- $m = row \mid n = column$ |
| 88 | +- $1 \leq i < m, \quad 1 \leq j < n$ |
| 89 | +- $\text{mem}(i,\; j) = \min(\text{mem}(i - 1,\; j), \text{mem}(i,\; j - 1)) + \text{cost}(i,\; j)$ |
| 90 | +- ![[image-DP-2.png]] |
| 91 | +- [[다이내믹 프로그래밍 완전 정복]] p.105 |
| 92 | + |
| 93 | +## 특정 점수에 도달하는 경우의 수 구하기 |
| 94 | +- 한번에 3, 5, 10점 얻을 수 있다 |
| 95 | +- 재귀의 경우 |
| 96 | + - ![[image-DP-3.png]] |
| 97 | +- 상향식 |
| 98 | + - $\text{for } i = 0, 1, \dots, N$ |
| 99 | + - $i - 3 \geq 0 \quad \text{arr}[i] = \text{arr}[i] + \text{arr}[i - 3]$ |
| 100 | + - $i - 5 \geq 0 \quad \text{arr}[i] = \text{arr}[i] + \text{arr}[i - 5]$ |
| 101 | + - $i - 10 \geq 0 \quad \text{arr}[i] = \text{arr}[i] + \text{arr}[i - 10]$ |
| 102 | + - $target = \text{arr}[N]$ |
| 103 | +- [[다이내믹 프로그래밍 완전 정복]] p.121 |
| 104 | +## 연속된 부분 배열의 최댓값 구하기 |
| 105 | +- 카데인 알고리즘(Kadane's algorithm) |
| 106 | + - $\text{M}(n) = \max(\text{M}(n - 1) + \text{arr}[n], \; \text{arr}(n) )$ |
| 107 | +- [[다이내믹 프로그래밍 완전 정복]] p.122 |
| 108 | +## 최소 교정 비용 문제 |
| 109 | +- 두 단어가 주어졌을 때, 두 단어가 똑같아 지는데 드는 교정 횟수 |
| 110 | +- 2차원 DP (각 두 단어의 차원) |
| 111 | + - $1 \leq i < m, \quad 1 \leq j < n$ |
| 112 | + - 첫 행, 열을 시퀀스하게 초기화 |
| 113 | + - 반대 글자가 비어있다면, 교정비용은 내 현재 글자 수만큼이다 |
| 114 | + - 같으면 |
| 115 | + - $dp[i][j] = dp[i - 1][j - 1]$ |
| 116 | + - 좌상 대각선 값 |
| 117 | + - 다르면 |
| 118 | + - $dp[i][j] = \min(dp[i - 1][j - 1],\; dp[i - 1][j],\; dp[i][j - 1]) + 1$ |
| 119 | + - 위쪽 셀, 왼쪽 셀, 왼쪽 위 셀의 값의 최솟 값 + 1 |
| 120 | + - $target = dp[m][n]$ |
| 121 | + - ![[image-DP-4.png]] |
| 122 | +- [[다이내믹 프로그래밍 완전 정복]] p.130 |
| 123 | + |
| 124 | +## 직사각형에서 총 경로 수 구하기 |
| 125 | +- M x N개의 방으로 구성된 직사각형이 있을 때, 좌상단 방에서 우하단 방까지 이동하는 모든 경로의 수 |
| 126 | +- 이동 - 오른쪽, 아래 한칸 씩 |
| 127 | +- 재귀 |
| 128 | + - ![[image-DP-5.png]] |
| 129 | + - 지수 시간 $2^{n}$ |
| 130 | +- 상향식 |
| 131 | + - 2차원 DP |
| 132 | + - $dp[i][j] = dp[i - 1][j] + dp[i][j - 1]$ |
| 133 | + - ![[image-DP-6.png]] |
| 134 | + - 다항 시간 $n^{2}$ |
| 135 | +- [[다이내믹 프로그래밍 완전 정복]] p.137 |
| 136 | + |
| 137 | +## 문자열 인터리빙 확인 문제 |
| 138 | +- 두 문자열 A, B의 모든 글자의 상대적인 순서가 유지 된채 섞여서 새로운 문자열 C가 만들어지면 이는 인터리빙이라 부른다 |
| 139 | + - A = 'xyz' |
| 140 | + - B = 'abcd' |
| 141 | + - C = 'xabyczd' - 인터리빙 |
| 142 | +- 재귀 |
| 143 | + - ![[image-DP-7.png]] |
| 144 | +- 상향식 |
| 145 | + - 2차원 DP (A, B) |
| 146 | + - A = 'bbca', B = 'bcc', C = 'bbcbcac' |
| 147 | + - 각 셀의 경우 (C의 현재 글자가) |
| 148 | + - A의 글자와 같고, B와 다를때 |
| 149 | + - 바로 위 셀값 (B가 다르니, B에 의존적) |
| 150 | + - B의 글자와 같고, A와 다를때 |
| 151 | + - 바로 왼쪽 셀값 (A가 다르니, A에 의존적) |
| 152 | + - A, B 둘다와 같을 때 |
| 153 | + - 왼쪽, 위쪽 셀값 중, True가 있으면 True |
| 154 | + - A, B 둘다와 다를 때 |
| 155 | + - False |
| 156 | + - ![[image-DP-8.png]] |
| 157 | +- [[다이내믹 프로그래밍 완전 정복]] p.144 |
| 158 | + |
| 159 | + |
| 160 | +## 부분집합의 합 구하기 |
| 161 | +- 0과 양의 정수로 이루어진 집합에서, 부분집합의 원소 합이 X인 것이 존재하는지 |
| 162 | +- c - {3, 2, 7, 1}, X = 6 |
| 163 | + - {3, 2, 1} |
| 164 | +- 재귀 |
| 165 | + - 포함하는 경우 || 포함하지 않는 경우 |
| 166 | + - $2^n$ |
| 167 | +- 상향식 |
| 168 | + - 2차원 DP |
| 169 | + - 행 - 집합 원소 |
| 170 | + - 열 - 타겟 넘버 까지의 시퀀스 |
| 171 | + - 중간 결과를 배열에 저장 |
| 172 | + - `subsum[i][j]` 는 부분집합의 첫 (i + 1)개의 원소로 구성된 합에 대해서 합이 j$(O \leq j \leq X)$인 부분집합이 있는지에 대한 `boolean` |
| 173 | + - `subsum[i][0]` 를 $v$ 로 정의 |
| 174 | + - 초기 조건 |
| 175 | + - ![[image-DP-10.png]] |
| 176 | + - 첫 행은 $v$와 `j`가 똑같으면 `T` |
| 177 | + - 첫 열은 공집합이기에 항상 `T` |
| 178 | + - 두 번째 행부터, $v$의 값만큼 위 셀에서 복사 |
| 179 | + - $v$의 값이 셀에 영향이 없기에 |
| 180 | + - 바로 위쪽 셀이 `T`면 `T` |
| 181 | + - $(i - 1, j - v)$ 셀의 값을 $(i, j)$로 복사 |
| 182 | + - ![[image-DP-11.png]] |
| 183 | + - $v$가 $X$보다 크다면 바로 위 행을 복사 |
| 184 | + - 타겟 넘버보다 크기에 |
| 185 | + - 아래 그림 $7$ 값인 3행은 위의 값을 그대로 복사 |
| 186 | + - ![[image-DP-9.png]] |
| 187 | +- `boolean` 대신 숫자로 한다면, 그 점수에 도달하는 가짓수 도출 가능 |
| 188 | + - 단 한번만 사용은 위에 행에서 `[i - 1][j - v] | [i - 1][j]` |
| 189 | + - 한번만 사용이니, 이전 행에 영향을 받는다 |
| 190 | + - 무제한 사용, 순서 상관 있는 경우 (`{1, 1, 2} != {2, 1, 1}`) |
| 191 | + - 자기 행에서 `[i][j - v] + [i - 1][j]` |
| 192 | + - 무제한 사용, 순서 상관 없는 경우의 타겟($k$) 만드는 갯수 |
| 193 | + - ```cpp |
| 194 | + dp[0] = 1; |
| 195 | + for (int i = 0; i < n; ++i) { // n = 동전 개수 |
| 196 | + int coin = coins[i]; |
| 197 | + for (int j = coin; j <= k; j++) { |
| 198 | + dp[j] += dp[j - coin]; |
| 199 | + } |
| 200 | + } |
| 201 | + cout << dp[k]; |
| 202 | +- [[다이내믹 프로그래밍 완전 정복]] p.152 |
| 203 | + |
| 204 | +## 거스름돈 최적화 (Coin Change) |
| 205 | +- 그리디로 해결 안되는 경우 ![[Greedy#^6d4b0e]] |
| 206 | +- 재귀 |
| 207 | + - `coin[i]` 는 사용 가능한 종류의 동전 액면가 |
| 208 | + - $\text{for } i = 0, 1, \dots, N$ |
| 209 | + - $minCoins(S) = 1 + \min(minCoins(S - coin[0]), minCoins(S - coin[1]), \dots, minCoins(S - coin[N - 1]))$ |
| 210 | +- 상향식 |
| 211 | + - ![[image-DP-14.png]] |
| 212 | + - 1원부터 ~ $S$원 까지 |
| 213 | + - 각 동전 별로 다시 순회하며 최소값 갱신 |
| 214 | + - `dp[n][T]` |
| 215 | + - `dp[i][j] = dp[i - 1][j]` |
| 216 | + - `dp[i][j] += dp[i][j - coin[i]]` |
| 217 | + - `n` is coin size |
| 218 | + - `T` is target number |
| 219 | + - 경우의 수를 구할땐 숫자로 기입, 존재 여부를 구할 땐 True, False |
| 220 | + - 기저 |
| 221 | + - `d[i][0]` = 1 or True |
| 222 | + - 메모리 최대 $O(T)$ 으로도 가능 |
| 223 | + - 직전 만 참조함으로, 벡터 2줄로 가능 `next_dp` |
| 224 | +- [[다이내믹 프로그래밍 완전 정복]] p.171 |
| 225 | + |
| 226 | +## 철근 자르기 |
| 227 | +- [[다이내믹 프로그래밍 완전 정복]] p.176 |
| 228 | + |
| 229 | +## 계단 오르기 - 2579 |
| 230 | +- 2개의 1차원 dp 배열 ($A, B$) |
| 231 | +- 연속 계단 3개 불가능 |
| 232 | +- $A = \text{직전 계단 사용 X}$ |
| 233 | +- $B = \text{직전 계단 사용 O}$ |
| 234 | +- $A_{n} = S[n] + \max(A_{n - 2},\;B_{n - 2})$ |
| 235 | +- $B_{n} = S[n] + S[n - 1] + MAX(A_{n - 3},\;B_{n - 3})$ |
| 236 | + |
| 237 | +## LIS (최장 증가 부분 수열 길이 구하기) |
| 238 | + - ```cpp |
| 239 | + int n; |
| 240 | + cin >> n; |
| 241 | + vector<int> a(n), dp(n, 1); |
| 242 | + for (int i = 0; i < n; i++) cin >> a[i]; |
| 243 | + |
| 244 | + for (int i = 1; i < n; i++) { |
| 245 | + for (int j = 0; j < i; j++) { |
| 246 | + if (a[j] < a[i]) { |
| 247 | + dp[i] = max(dp[i], dp[j] + 1); |
| 248 | + } |
| 249 | + } |
| 250 | + } |
| 251 | + |
| 252 | + cout << *max_element(dp.begin(), dp.end()); |
| 253 | +- `dp[i]`는 `a[i]`를 마지막 원소로 갖는 LIS의 길이 |
| 254 | +- 현재 글자의, 이전 글자들 대소 비교로, dp 업데이트 |
| 255 | + - 이미 왼쪽부터 dp가 정복하니 최적해 보장 |
| 256 | +- $O(N^{2})$ |
0 commit comments