動態規劃 (DP) 是解決多階段決策最優化問題的核心演算法。本文以經典的「數字塔」問題引入 DP 思維,進而深入講解 0/1 背包問題的狀態轉移方程,並展示如何從二維 DP 優化至一維陣列,達到極致時空效率。
有 N 個物品,第 i 個佔體積 need[i]、價值 value[i]。背包容量 V。每個物品只能選 0 或 1 個。求最大總價值。
定義 dp[i][j]:前 i 個物品放入容量 j 的背包最大價值。
if (j < need[i]) // 第 i 件裝不下
dp[i][j] = dp[i-1][j];
else // 取 max(不放, 放入)
dp[i][j] = max(dp[i-1][j], dp[i-1][j-need[i]] + value[i]);
邊界:dp[0][j] = 0(0 件物品價值為 0)
4 件物品:
| i | need | value |
|---|---|---|
| 1 | 2 | 2 |
| 2 | 3 | 5 |
| 3 | 4 | 3 |
| 4 | 1 | 2 |
背包容量 V=5。填表過程:
i\j 0 1 2 3 4 5
0 0 0 0 0 0 0
1 0 0 2 2 2 2
2 0 0 2 5 5 7
3 0 0 2 5 5 7
4 0 2 2 5 7 7
答案:dp[4][5] = 7(選物品 2+4,體積 3+1=4,價值 5+2=7)
int dp[N+1][V+1];
for (int i = 1; i <= N; ++i)
for (int j = 1; j <= V; ++j)
dp[i][j] = (j < need[i]) ? dp[i-1][j]
: max(dp[i-1][j], dp[i-1][j-need[i]] + value[i]);
觀察轉移方程:dp[i][*] 僅依賴 dp[i-1][*]。只需兩行交替:
int dp[2][V+1];
for (int i = 1; i <= N; ++i) {
int cur = i & 1, prev = cur ^ 1;
for (int j = 1; j <= V; ++j)
dp[cur][j] = (j < need[i]) ? dp[prev][j]
: max(dp[prev][j], dp[prev][j-need[i]] + value[i]);
}
int dp[V+1] = {0};
for (int i = 1; i <= N; ++i)
for (int j = V; j >= need[i]; --j) // 關鍵:從大到小!
dp[j] = max(dp[j], dp[j - need[i]] + value[i]);
dp[j] 依賴同一輪的 dp[j - need[i]](舊值)。順序遍歷會提前覆蓋舊值,導致同一物品被重複放入(變成完全背包)。逆序保證讀到的 dp[j - need[i]] 仍是上一輪(i-1)的值。
#include <stdio.h>
#include <stdlib.h>
#define MAX(a,b) ((a)>(b)?(a):(b))
int main() {
int n = 4, V = 5;
int need[] = {0, 2, 3, 4, 1}; // 1-based
int value[] = {0, 2, 5, 3, 2};
int dp[V+1] = {0};
for (int i = 1; i <= n; ++i) {
for (int j = V; j >= need[i]; --j) {
dp[j] = MAX(dp[j], dp[j - need[i]] + value[i]);
}
}
printf("最大價值: %d\n", dp[V]); // 輸出 7
return 0;
}
| 版本 | 時間 | 空間 | 備註 |
|---|---|---|---|
| 二維 DP | O(NV) | O(NV) | 教學最直觀 |
| 滾動陣列 | O(NV) | O(V) | 實務常用 |
| 一維逆序 | O(NV) | O(V) | 極致優化,三行核心代碼 |
j = need[i] to V