- 이 글은 [PAST] Part 2 — 동적 계획법 심화 ① 의 보충편입니다. 본편에서 지면상 접어 둔 곳을 펼칩니다.
- 소재는 「アルゴリズム実技検定 公式テキスト [上級]~[エキスパート]編」(이하 "서적")의 예제 2-1-1, 즉 AtCoder Educational DP Contest D문제입니다.
- 본 시리즈의 코드 예제는 Java로 재작성되었습니다. 서적의 원본 소스코드(Python)는 Github 리포지토리에 있습니다.
이 글을 쓰는 이유
본편에서 배낭 문제는 "복습"이라는 이름으로 지나갔습니다. 정의를 세우고, 점화식을 두 갈래로 나누고, Java 구현 두 개를 붙이는 데까지가 전부였습니다. 그런데 배낭 문제에서 실제로 시간을 잡아먹는 것은 그 뼈대가 아니라 뼈대에 붙어 있는 자잘한 판단들입니다.
- 왜 답을
dp[N][W]한 칸에서 읽지 않고 마지막 행 전체를 훑는가? - 1차원으로 접을 때 "역순으로 돌아라"는 말은 알겠는데, 정순으로 돌리면 구체적으로 어느 칸이 어떻게 망가지는가?
- 최댓값 말고 어떤 품목을 골랐는지는 어떻게 알아내는가?
- 제약이 바뀌면 이 풀이는 어디서부터 무너지는가?
이 글은 그 네 가지에 답합니다. 본편과 겹치는 부분은 2절에서 다섯 줄로 압축하고 넘어가겠습니다.
1. 문제를 다시 읽는다 — 제약이 설계를 결정한다
$N$개의 품목이 있고 품목 $i$의 무게는 $w_i$, 가치는 $v_i$입니다. 무게의 총합이 $W$ 이하가 되도록 몇 개를 골라, 가치의 총합의 최댓값을 구합니다.
여기까지는 누구나 읽습니다. 중요한 것은 그 아래 붙어 있는 제약 네 줄입니다. 경험이 쌓이면 이 네 줄만 보고도 구현의 골격이 거의 정해집니다.
| 제약 | 이것이 결정하는 것 |
| $1 \leq N \leq 100$ | DP 표의 행 수가 $N+1 = 101$. $2^N$ 전탐색은 논외 |
| $1 \leq W \leq 10^5$ | DP 표의 열 수가 $W+1$. 표 전체가 약 $10^7$칸 — 시간도 메모리도 여기서 나온다 |
| $1 \leq w_i \leq W$ | 어떤 품목도 혼자서는 반드시 들어간다. "너무 무거워 못 넣는 품목"이라는 예외를 따로 처리할 필요가 없다 |
| $1 \leq v_i \leq 10^9$ | 가치의 총합이 최대 $100 \times 10^9 = 10^{11}$. int의 상한 약 $2.1 \times 10^9$을 50배 가까이 넘는다 → long |
습관으로 만들 것 — 제약의 곱을 먼저 암산한다
$N \times W = 10^7$은 "표를 다 채워도 된다"는 허가이고, $N \times \max v_i = 10^{11}$은 "int를 쓰면 죽는다"는 경고입니다. 문제를 읽자마자 이 두 곱을 계산해 두면, 나중에 원인 모를 WA를 붙들고 있을 일이 줄어듭니다. Python으로 짜다 Java로 옮길 때 특히 그렇습니다 — Python의 정수에는 상한이 없어서 이 경고가 아예 보이지 않기 때문입니다.
2. 본편 요약 — 여기까지는 이미 한 이야기
본편에서 세운 정의와 점화식만 옮겨 둡니다. 자세한 유도 과정은 본편 2.1절을 봐 주세요.
dp[i][j] ← 번호가 작은 i개의 품목(품목 0, 1, ..., i-1)만 고려하여,
무게의 총합이 정확히 j가 되도록 몇 개 골랐을 때 가치의 총합의 최댓값
(무게의 총합을 j로 만들 수 없는 경우는 -∞)
- 초기 조건 —
dp[0][0] = 0, 나머지는 $-\infty$ - 품목 $i$를 고르지 않는다 — $dp[i+1][j] = dp[i][j]$
- 품목 $i$를 고른다 — $dp[i+1][j] = dp[i][j - w_i] + v_i$ (단 $j \geq w_i$)
- 답 — 마지막 행 전체의 최댓값 $\max_j dp[N][j]$
- 계산량 — $O(NW)$
이 정의를 정의 A라고 부르겠습니다. 4절에서 다른 정의와 비교할 것이기 때문입니다.
3. 손으로 표를 채워 본다
점화식을 읽어서 이해한 기분이 드는 것과, 표가 실제로 어떻게 채워지는지 아는 것은 다릅니다. 서적과 AtCoder가 함께 제시하는 예제 입력을 그대로 쓰겠습니다.
3 8 ← N = 3, W = 8
3 30 ← 품목 0: 무게 3, 가치 30
4 50 ← 품목 1: 무게 4, 가치 50
5 60 ← 품목 2: 무게 5, 가치 60
정답은 90입니다. 품목 0과 2를 고르면 무게 $3 + 5 = 8$, 가치 $30 + 60 = 90$이 됩니다.
이제 정의 A의 표를 한 행씩 채웁니다. $i$번째 행은 "품목 $i-1$까지 처리한 직후"의 상태입니다. $i=0$행이 "아무것도 보지 않은 상태"라는 점에 주의하세요.
| $i$ | $j=0$ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 0 (시작) | 0 | −∞ | −∞ | −∞ | −∞ | −∞ | −∞ | −∞ | −∞ |
| 1 (품목 0 처리 후) | 0 | −∞ | −∞ | 30 | −∞ | −∞ | −∞ | −∞ | −∞ |
| 2 (품목 1 처리 후) | 0 | −∞ | −∞ | 30 | 50 | −∞ | −∞ | 80 | −∞ |
| 3 (품목 2 처리 후) | 0 | −∞ | −∞ | 30 | 50 | 60 | −∞ | 80 | 90 |
몇 칸만 소리 내어 읽어 봅시다
- $dp[1][3] = 30$ — 품목 0(무게 3)만으로 무게 3을 만드는 유일한 방법입니다. $dp[0][3 - 3] + 30 = dp[0][0] + 30 = 30$.
- $dp[2][7] = 80$ — 품목 1을 쓰는 갈래에서 $dp[1][7 - 4] + 50 = dp[1][3] + 50 = 30 + 50 = 80$. 즉 품목 0과 1을 둘 다 고른 상태입니다. 표의 한 칸이 조합 하나를 대표하고 있는 셈입니다.
- $dp[3][8] = 90$ — $dp[2][8 - 5] + 60 = dp[2][3] + 60 = 30 + 60 = 90$. 품목 0과 2의 조합이고, 이것이 답입니다.
$j = 6$ 열은 끝까지 $-\infty$ 다
무게 3, 4, 5로 정확히 6을 만들 수 있는 조합이 없기 때문입니다(3+4=7, 3+5=8, 4+5=9). 정의 A는 "정확히 $j$"를 요구하므로 이런 빈칸이 생기는 것이 정상입니다.
그리고 바로 이 빈칸 때문에 답을 dp[N][W] 한 칸에서 읽으면 안 됩니다. 이번 입력에서는 우연히 $dp[3][8]$에 답이 있었지만, 만약 $W = 6$이었다면 $dp[3][6] = -\infty$이고 실제 답은 $dp[3][5] = 60$입니다. 본편의 코드가 마지막 행 전체를 훑는 이유가 이것입니다.
4. 정의를 바꿔 본다 — "정확히 $j$" vs "$j$ 이하"
3절 끝의 불편함("답을 읽으려고 행 전체를 훑어야 한다")은 정의를 조금 바꾸면 사라집니다.
dp[i][j] ← 번호가 작은 i개의 품목만 고려하여,
무게의 총합이 j 이하가 되도록 골랐을 때 가치의 총합의 최댓값
이것을 정의 B라 부릅시다. 점화식은 정의 A와 글자 하나 다르지 않습니다. 바뀌는 것은 앞뒤 두 군데뿐입니다.
| 정의 A — 정확히 $j$ | 정의 B — $j$ 이하 | |
| 초기화 | dp[0][0] = 0, 나머지 $-\infty$ |
전부 0 |
| 점화식 | 동일 | |
| 답을 읽는 곳 | max(dp[N][0..W]) |
dp[N][W] 한 칸 |
같은 입력으로 정의 B의 표를 채우면 이렇게 됩니다.
| $i$ | $j=0$ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 0 (시작) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | 0 | 30 | 30 | 30 | 30 | 30 | 30 |
| 2 | 0 | 0 | 0 | 30 | 50 | 50 | 50 | 80 | 80 |
| 3 | 0 | 0 | 0 | 30 | 50 | 60 | 60 | 80 | 90 |
두 표의 마지막 행을 나란히 놓으면 관계가 한눈에 보입니다.
정의 A: 0 -∞ -∞ 30 50 60 -∞ 80 90
정의 B: 0 0 0 30 50 60 60 80 90
↑
A 의 j=6 은 만들 수 없는 칸이지만,
B 의 j=6 은 "무게 5 로 얻은 60 을 그대로 들고 있다"
즉 정의 B의 마지막 행은 정의 A 마지막 행의 누적 최댓값(prefix max)입니다. 정의 B는 "행 전체를 훑는" 작업을 표를 채우는 과정 안으로 흡수해 버린 것입니다.
그럼 어느 쪽을 쓸 것인가
- 정의 B가 코드는 짧습니다. 초기화가
0이라Arrays.fill도 필요 없고 답도 한 칸에서 읽습니다. 배낭 문제만 풀 거라면 B가 편합니다. - 정의 A는 "무게의 총합을 정확히 $j$로 만들 수 있는가"라는 정보를 표 안에 보존합니다. "무게 합계가 정확히 $X$인 조합이 존재하는가", "가능한 무게 합계를 전부 나열하라" 같은 변형이 붙으면 A만 답할 수 있습니다.
본편이 A를 쓴 것은 서적을 따랐기 때문이고, 서적이 A를 고른 것은 2.2절 이후의 문제들이 전부 "정확히 이 상태"를 첨자로 들고 가는 형태이기 때문입니다. 배낭 하나만 놓고 보면 B가 편하지만, 시리즈 전체를 관통하는 사고방식은 A 쪽입니다.
5. 1차원으로 접기 — 정순으로 돌리면 무슨 일이 벌어지는가
본편에서 2차원 배열이 $101 \times 100001$개의 long, 약 80MB를 차지한다고 했습니다. dp[i+1]이 dp[i]만을 참조하므로 1차원으로 접을 수 있고, 그때 $j$를 큰 쪽부터 돌아야 한다고 했습니다.
long[] dp = new long[W + 1];
Arrays.fill(dp, NEG);
dp[0] = 0;
for (int i = 0; i < N; i++) {
// j 를 역순으로 — 같은 품목을 두 번 담는 것을 막는다
for (int j = W; j >= w[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
}
}
"같은 품목을 두 번 담게 된다"는 설명은 맞지만, 추상적입니다. 실제로 어느 칸이 언제 망가지는지 같은 입력으로 확인해 봅시다.
역순으로 돌린 경우 (정답)
| 시점 | $j=0$ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 품목 0 처리 후 | 0 | −∞ | −∞ | 30 | −∞ | −∞ | −∞ | −∞ | −∞ |
| 품목 1 처리 후 | 0 | −∞ | −∞ | 30 | 50 | −∞ | −∞ | 80 | −∞ |
| 품목 2 처리 후 | 0 | −∞ | −∞ | 30 | 50 | 60 | −∞ | 80 | 90 |
3절의 2차원 표에서 $i=1, 2, 3$행을 그대로 떼어 온 것과 완전히 같습니다. 접기가 성공한 것입니다.
정순으로 돌린 경우 (반례)
루프를 for (int j = w[i]; j <= W; j++)로 바꾸기만 하면 이렇게 됩니다.
| 시점 | $j=0$ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 품목 0 처리 후 | 0 | −∞ | −∞ | 30 | −∞ | −∞ | 60 | −∞ | −∞ |
| 품목 1 처리 후 | 0 | −∞ | −∞ | 30 | 50 | −∞ | 60 | 80 | 100 |
| 품목 2 처리 후 | 0 | −∞ | −∞ | 30 | 50 | 60 | 60 | 80 | 100 |
답이 100으로 나옵니다. 정답 90보다 큽니다.
망가지는 순간을 정확히 짚을 수 있습니다. 첫 행의 $j = 6$입니다. 품목 0 하나를 처리하는 동안, $j = 3$에서 dp[3] = 30으로 갱신한 뒤 루프가 계속 올라가 $j = 6$에 도달하면
$$dp[6] = dp[6 - 3] + 30 = dp[3] + 30 = 30 + 30 = 60$$
을 계산합니다. 이때 읽은 dp[3]은 방금 이 루프가 품목 0을 써서 만든 값입니다. 그러니 $dp[6] = 60$은 "품목 0을 두 번 담았다"는 뜻입니다. 역순으로 돌면 $j = 6$을 $j = 3$보다 먼저 처리하므로, dp[3]은 아직 "품목 0을 쓰기 전"의 값($-\infty$)이고 오염이 일어나지 않습니다.
그 뒤 $j = 8$의 100은 품목 1(무게 4, 가치 50)을 두 번 담은 값입니다. 확인해 보면 이 정순 코드가 내놓는 답은 무한 개수 배낭 문제(unbounded knapsack)의 정답과 정확히 일치합니다.
뒤집어 말하면 — 정순이 정답이 되는 문제도 있다
"각 품목을 원하는 만큼 여러 번 담을 수 있다"는 문제(무한 개수 배낭)에서는, 정순 루프가 그대로 정답 코드입니다. 같은 한 줄이 루프 방향 하나로 두 문제를 오갑니다.
// 0-1 배낭 — 각 품목을 최대 1개
for (int j = W; j >= w[i]; j--) dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
// 무한 개수 배낭 — 각 품목을 몇 개든
for (int j = w[i]; j <= W; j++) dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
그래서 1차원 배낭 코드를 볼 때는 루프의 방향부터 확인하는 습관을 들이는 게 좋습니다. 그 화살표 하나가 문제의 종류를 말해 줍니다.
6. 어떤 품목을 골랐는가 — 복원
대회에서는 최댓값만 출력하면 끝이지만, 현실의 최적화 문제에서 "최댓값은 90입니다"만으로 끝나는 경우는 없습니다. 무엇을 골라야 90이 되는지를 답해야 합니다.
2차원 표를 남겨 두었다면 복원은 어렵지 않습니다. 마지막 행에서 답이 있는 칸을 찾아 거꾸로 거슬러 올라가면서, 각 단계에서 품목 $i$를 썼는지 판정하면 됩니다.
// dp 는 3절의 2차원 표 (정의 A)
private static List<Integer> restore(long[][] dp, int N, int W, int[] w) {
// 답이 있는 칸부터 시작한다 — 마지막 행의 최댓값 위치
int j = 0;
for (int t = 0; t <= W; t++) {
if (dp[N][t] > dp[N][j]) j = t;
}
List<Integer> chosen = new ArrayList<>();
for (int i = N - 1; i >= 0; i--) {
// dp[i+1][j] 가 dp[i][j] 와 다르다면, 그 값은 「품목 i 를 고르는」
// 갈래에서 온 것이다 = 품목 i 를 썼다
if (dp[i + 1][j] != dp[i][j]) {
chosen.add(i);
j -= w[i]; // 품목 i 의 무게만큼 되돌린다
}
}
Collections.reverse(chosen);
return chosen; // [0, 2]
}
예제 입력으로 돌리면 [0, 2]가 나옵니다. 무게 $3 + 5 = 8$, 가치 $30 + 60 = 90$ — 서적이 설명한 최적해와 일치합니다.
두 가지 주의
- 동점일 때 — 품목 $i$를 써도 안 써도 같은 값이 나오는 칸에서는
dp[i+1][j] == dp[i][j]가 되어 "안 썼다"로 판정합니다. 최적해가 여러 개일 때 그중 하나를 고르는 것이므로 정답이지만, "사전순으로 가장 빠른 조합"처럼 특정 해를 요구한다면 판정 기준을 손봐야 합니다. - 1차원으로 접으면 복원할 수 없습니다. 5절의 롤링 배열은 과거 행을 덮어써 버리므로 거슬러 올라갈 표가 남지 않습니다. 메모리 80MB를 아끼는 대가로 "무엇을 골랐는가"를 포기하는 셈입니다. 둘 다 필요하면 2차원을 남겨야 합니다.
7. 제약이 뒤집히면 — 첨자도 뒤집는다 (EDPC E)
지금까지의 풀이는 $O(NW)$였습니다. 이 계산량은 $W$가 작다는 데 전적으로 기대고 있습니다. 그러면 $W$가 커지면 어떻게 될까요?
문제문은 D와 한 글자도 다르지 않습니다. 다른 것은 제약뿐입니다.
| D - Knapsack 1 | E - Knapsack 2 | |
| $N$ | $\leq 100$ | $\leq 100$ |
| $W$ | $\leq 10^5$ | $\leq 10^9$ 😱 |
| $v_i$ | $\leq 10^9$ | $\leq 10^3$ |
| $O(NW)$ | $10^7$ — 통과 | $10^{11}$ — 불가능 |
$W$를 첨자로 쓸 수 없게 되었습니다. 그런데 제약을 다시 보면, $v_i$ 쪽이 작아졌습니다. 가치의 총합은 최대 $100 \times 10^3 = 10^5$입니다. 방금 $W$가 있던 자리에 딱 맞는 크기입니다.
여기서 이 시리즈의 핵심 감각이 나옵니다. 무엇을 첨자로 들고 갈지는 우리가 고르는 것이 아니라 제약이 정해 줍니다. 그러니 무게와 가치의 역할을 통째로 맞바꿉시다.
dp[i][x] ← 앞의 i개 품목만 고려하여,
가치의 총합이 정확히 x가 되도록 골랐을 때 무게의 총합의 최솟값
(가치의 총합을 x로 만들 수 없는 경우는 +∞)
"최댓값을 구하라"는 문제가 최솟값 DP로 바뀌었습니다. 답은 "무게가 $W$ 이하인 $x$ 중 가장 큰 것"입니다. 같은 예제 입력으로 채워 보면 이렇게 됩니다.
| 가치 $x$ | 0 | 30 | 50 | 60 | 80 | 90 | 110 | 140 |
| 최소 무게 $dp[3][x]$ | 0 | 3 | 4 | 5 | 7 | 8 | 9 | 12 |
| $W = 8$ 이하인가 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ |
(표에 없는 $x$는 전부 $+\infty$ — 만들 수 없는 가치입니다.) ✓가 붙은 것 중 가장 오른쪽이 $x = 90$이고, 이것이 답입니다. D와 같은 90이 나왔습니다.
import java.io.*;
import java.util.*;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int N = Integer.parseInt(st.nextToken());
long W = Long.parseLong(st.nextToken()); // 10^9 까지 — int 도 되지만 long 이 안전하다
int[] w = new int[N];
int[] v = new int[N];
int V = 0; // 가치의 총합 = 첨자의 상한
for (int i = 0; i < N; i++) {
st = new StringTokenizer(br.readLine());
w[i] = Integer.parseInt(st.nextToken());
v[i] = Integer.parseInt(st.nextToken());
V += v[i];
}
// 무게의 총합은 최대 100 * 10^9 = 10^11 이므로 long
final long INF = Long.MAX_VALUE / 4;
// dp[x] = 가치의 총합이 정확히 x 일 때 무게의 최솟값
long[] dp = new long[V + 1];
Arrays.fill(dp, INF);
dp[0] = 0;
for (int i = 0; i < N; i++) {
// 5절과 같은 이유로 역순 — 같은 품목을 두 번 쓰지 않기 위해
for (int x = V; x >= v[i]; x--) {
if (dp[x - v[i]] < INF) {
dp[x] = Math.min(dp[x], dp[x - v[i]] + w[i]);
}
}
}
// 무게가 W 이하인 것 중 가장 큰 가치
int ans = 0;
for (int x = 0; x <= V; x++) {
if (dp[x] <= W) ans = x;
}
System.out.println(ans);
}
}
계산량은 $O(N \sum v_i) = 100 \times 10^5 = 10^7$ — D와 똑같은 규모로 돌아왔습니다.
이것이 본편 2.2절의 주제다
본편 2.2절은 dp[i][(어떤 상태)]라는 템플릿을 세우고, "(어떤 상태) 자리에 무엇을 넣을 것인가"만 궁리하면 수많은 문제가 풀린다고 했습니다. D와 E는 같은 문제의 (어떤 상태) 자리를 서로 바꿔 끼운 한 쌍입니다.
- D — 상태 = 고른 품목의 무게의 총합, 값 = 가치의 최댓값
- E — 상태 = 고른 품목의 가치의 총합, 값 = 무게의 최솟값
"상태로 쓸 수 있는 것"과 "값으로 둘 것"은 제약의 크기가 정합니다. 작은 쪽이 첨자로, 큰 쪽이 값으로 갑니다. 새 문제에서 $O(N \times \text{무언가})$가 터진다면, 그 "무언가"의 자리에 들어갈 다른 후보가 없는지부터 찾아보세요.
8. 자주 틀리는 지점 5선
제출 전 체크리스트
- 가치를
int로 받았다 — $100 \times 10^9 = 10^{11}$은int의 상한을 50배 가까이 넘습니다. 조용히 음수로 뒤집혀 WA가 됩니다. 1절의 곱셈을 먼저 하세요. - 정의 A인데 답을
dp[N][W]에서 읽었다 — 그 칸이 $-\infty$일 수 있습니다(3절의 $j=6$). 마지막 행 전체의 최댓값을 취하거나, 아예 정의 B로 가세요. - 1차원 롤링을 정순으로 돌렸다 — 무한 개수 배낭이 되어 답이 커집니다. 5절의 반례에서 90이 100이 되었습니다. 값이 정답보다 크게 나오면 이걸 가장 먼저 의심하세요.
NEG를Long.MIN_VALUE로 두었다 — 도달 불가능한 칸에도dp[j - w[i]] + v[i]가 더해지므로 오버플로가 나고,Math.max가 그 거대한 양수를 골라 버립니다.-(1L << 60)처럼 더해도 여전히 압도적으로 작은 값을 쓰세요. E의INF도 같은 이유로Long.MAX_VALUE / 4입니다.- 2차원 배열을 습관적으로 잡았다 — $101 \times 100001$개의
long은 약 80MB입니다. D의 메모리 제한에는 들어가지만, $W$가 조금만 더 커지면 바로 MLE입니다. 복원이 필요 없다면 1차원으로 접는 편이 안전합니다.
9. 연습 문제
같은 사고방식이 통하는 문제들입니다. 위에서 아래로 갈수록 "첨자에 무엇을 들고 갈 것인가"를 스스로 정해야 하는 폭이 커집니다.
| 문제 | 출처 | 보는 곳 | 난이도 |
| D - Knapsack 1 | Educational DP Contest | 이 글의 본체 | ★★ |
| E - Knapsack 2 | Educational DP Contest | 7절 — 첨자 뒤집기 | ★★★ |
| D - ナップサック問題 | AtCoder Beginner Contest 032 | 제약이 세 갈래로 갈린다 — 어느 풀이를 쓸지 제약을 보고 직접 고르는 문제 | ★★★★ |
| E - All-you-can-eat | AtCoder Beginner Contest 145 | 배낭 + 순서를 정하는 궁리가 한 겹 더 붙는다 | ★★★★ |
정리
- 제약을 먼저 곱해 보면 표의 크기와 자료형이 그 자리에서 정해집니다. $N \times W$는 시간, $N \times \max v_i$는
int/long을 결정합니다. - "정확히 $j$"와 "$j$ 이하"는 점화식이 같고 초기화와 답 읽는 위치만 짝을 이뤄 바뀝니다. 후자의 마지막 행은 전자의 누적 최댓값입니다.
- 1차원 롤링의 루프 방향은 문제의 종류를 말해 줍니다. 역순이면 0-1 배낭, 정순이면 무한 개수 배낭입니다.
- 복원은 2차원 표가 남아 있을 때만 가능합니다. 메모리를 아끼면 "무엇을 골랐는가"를 잃습니다.
- 첨자에 무엇을 들고 갈지는 제약이 정합니다. $O(N \times \text{무언가})$가 터지면, 그 자리에 들어갈 더 작은 후보를 찾으세요.
참고 자료
- 본편: [PAST] Part 2 — 동적 계획법 심화 ①
- 서적: 「アルゴリズム実技検定 公式テキスト [上級]~[エキスパート]編」 (マイナビ出版, 2023) — 제2장 2.1절, 예제 2-1-1
- 소스코드: tsutaj/pastbook-2-source-code (Github)
- Educational DP Contest: https://atcoder.jp/contests/dp
- AtCoder 공식 사이트: https://atcoder.jp
다음 보충편 예고
[PAST] Part 2 보충 — AtCounter 문제 풀이 에서는 $N \leq 10^6$이라는 제약이 구현을 어떻게 좁히는지, 그리고 이번 5절에서 본 역순 갱신이 왜 문자열 문제에서도 똑같이 필요한지를 다룹니다.