Algorithm - DP Knapsack
想參考 DP 背包問題的各種實戰題型的話,我有整理底家,直接開打 XD:
LeetCode - 0/1 Knapsack Problems LeetCode - Complete Knapsack Problems LeetCode - Multiple Knapsack ProblemsDP Knapsack Note
背包其實就三種你要會分:
| 類型 | 特徵 |
|---|---|
| 0/1 背包 | 每個物品只能用一次 |
| 完全背包 | 每個物品可以用無限次(Coin Change) |
| 多重背包 | 每個物品有數量限制 |
背包問題的本質不是「選物品」,而是:在容量限制下,做選擇,並且把問題拆成子問題
幾乎所有背包題都可以轉換成:
1 | |
背包問題的核心只有兩件事:
1 | |
0/1 背包 (每個物品只能使用一次)
適用於:
- 每個物品只能使用一次
- subset / partition 類問題
- 選或不選某個元素
1 | |
特性
j從大到小- 避免同一個物品在同一輪被使用多次
- 核心是「選或不選」
1 | |
所以轉移是:
1 | |
為什麼 0/1 背包要倒著跑?
因為每個物品只能用一次。
假設正著跑:
1 | |
那 dp[j] 可能會用到這一輪剛更新過的 dp[j - weight[i]],這樣就等於同一個物品被重複使用了。
所以 0/1 背包必須倒著跑:
1 | |
這樣才能確保 dp[j - weight[i]] 還是上一輪的狀態,也就是還沒使用第 i 個物品以前的結果。
value[i] 要加什麼?
1 | |
關鍵:
+什麼 = 你想最大化的東西
dp 存什麼 → 就加什麼
例子:
最大數量(474) → +1
最大總和(1049) → +weight[i]
最大價值(一般背包) → +value[i]
完全背包 (每個物品可以使用無限次)
適用於:
- 每個物品可以使用無限次
- coin change 類問題
- 題目明確說可以重複使用
1 | |
特性
j從小到大- 允許同一個物品被重複使用
- 會用到這一輪已經更新過的
dp[j - weight[i]]
為什麼完全背包要正著跑?
因為每個物品可以用無限次。
正著跑時:
1 | |
dp[j] 可以用到同一輪剛更新過的 dp[j - weight[i]]。
這代表:
1 | |
所以完全背包要正著跑。
從 amount 角度寫完全背包
有些題目也可以從金額或容量角度思考。
以 Coin Change 為例:
1 | |
可以想成:
1 | |
模板:
1 | |
這種寫法很直覺,因為它是從小金額一路推到大金額。
多重背包 (每個物品有使用次數限制)
適用於:
- 每個物品有使用次數限制
- 例如某個物品最多只能用
k次
最直覺的想法是:
1 | |
但直接枚舉次數可能會太慢。
常見做法是把多重背包轉成 0/1 背包。
例如某個物品有 13 個:
1 | |
所以可以拆成:
1 | |
每一組都只能選一次,這樣就變成 0/1 背包。
如何判斷是哪一種背包
解題時先問:
1 | |
Step 1:判斷物品能不能重複使用
1 | |
Step 2:判斷 dp 的意義
常見有三種:
1 | |
例如:
1 | |
所以是:
1 | |
1 | |
所以是:
1 | |
1 | |
所以是:
1 | |
Step 3:判斷 j 的方向
| 類型 | 物品能不能重複用 | j 的方向 |
|---|---|---|
| 0/1 背包 | 不能 | 由大到小 |
| 完全背包 | 可以 | 由小到大 |
| 多重背包 | 有限制 | 拆成 0/1 背包 |
判斷更新方式的核心原則
關鍵問題是:
這個物品在同一輪能不能被再次使用?
如果不能重複使用:
1 | |
如果可以重複使用:
1 | |
所以背包問題最重要的記憶點是:
1 | |
最少數量型背包
例如:
1 | |
題目問:
1 | |
dp 定義:
1 | |
初始化:
1 | |
轉移:
1 | |
因為 coin 可以無限次使用,所以是完全背包,j 正著跑:
1 | |
如果最後:
1 | |
代表湊不出來。
方法數型背包
例如:
1 | |
題目問:
1 | |
dp 定義:
1 | |
初始化:
1 | |
意思是:
1 | |
轉移:
1 | |
完整模板:
1 | |
這裡外層先枚舉 coin,內層再枚舉 amount,代表算的是「組合數」,不是「排列數」。
組合數與排列數的差別
這是背包題裡很容易混淆的地方。
如果題目問組合數:
1 | |
算同一種。
通常寫法是:
1 | |
如果題目問排列數:
1 | |
算不同種。
通常寫法是:
1 | |
所以要記得:
1 | |
Boolean 型背包
例如:
1 | |
題目問:
1 | |
dp 定義:
1 | |
初始化:
1 | |
轉移:
1 | |
因為每個數字只能用一次,所以是 0/1 背包,j 倒著跑:
1 | |
常見對應關係
| 題型 | 背包類型 | dp 意義 | 操作 |
|---|---|---|---|
| 322. Coin Change | 完全背包 | 最少硬幣數 | min |
| 518. Coin Change II | 完全背包 | 組合數 | + |
| 416. Partition Equal Subset Sum | 0/1 背包 | 是否能湊出 target | boolean |
| 494. Target Sum | 0/1 背包 | 方法數 | + |
| 279. Perfect Squares | 完全背包 | 最少平方數數量 | min |
| 377. Combination Sum IV | 完全背包變形 | 排列數 | + |
經典題目
| 類型 | 題目 | 備註 |
|---|---|---|
| 0/1 背包 | 416. Partition Equal Subset Sum | 每個數字只能選一次,判斷是否能湊出 sum / 2 |
| 0/1 背包 | 494. Target Sum | 每個數字只能用一次,轉成「有多少種方法湊出某個 target」 |
| 0/1 背包 | 474. Ones and Zeroes | 每個字串只能選一次,限制有兩個容量:0 的數量與 1 的數量 |
| 0/1 背包 | 1049. Last Stone Weight II | 每個石頭只能選一次,目標是讓兩堆重量差最小 |
| 0/1 背包 | 2915. Length of the Longest Subsequence That Sums to Target | 每個元素只能選一次,求湊出 target 的最長 subsequence |
| 0/1 背包 | 2787. Ways to Express an Integer as Sum of Powers | 每個數字的 power 最多只能用一次,求湊出 n 的方法數 |
| 完全背包 | 139. Word Break | wordDict 裡的字可以重複使用,判斷字串是否能被組成 |
| 完全背包 | 322. Coin Change | 每種 coin 可以無限次使用,求最少硬幣數 |
| 完全背包 | 518. Coin Change II | 每種 coin 可以無限次使用,求組合數 |
| 完全背包 | 279. Perfect Squares | 每個 perfect square 可以無限次使用,求最少數量 |
| 完全背包 | 377. Combination Sum IV | 每個數字可以重複使用,且順序不同算不同方法,屬於排列型完全背包 |
| 多重背包 / 變形背包 | 1155. Number of Dice Rolls With Target Sum | 有固定 dice 數,每顆骰子只能選一個面,屬於有限次選擇的變形背包 |
| 多重背包 / 變形背包 | 879. Profitable Schemes | 每個 crime 只能選一次,但有兩個限制維度:人數與 profit,屬於 0/1 背包變形 |
建議記憶方式
優先記兩個模板:
1 | |
然後再根據題目問什麼,決定轉移方式:
1 | |
解背包題時,優先做這件事:
Step 1:先判斷物品能不能重複用
1 | |
Step 2:看題目在問什麼
1 | |
Step 3:判斷是否有「順序影響」
1 | |
總結
背包問題可以統一成:
1 | |
核心永遠是:
1 | |
最後的轉移通常長得像:
1 | |
差別只在:
1 | |
背包題最重要的觀念是:
迴圈方向決定同一個物品能不能被重複使用
只要能判斷這一點,大部分背包題就不會亂。