最近在牛客刷題遇到好幾道背包問題,索性這兩天集中火力刷了一些這類的題。這里總結一下0-1背包、完全背包和多重背包三種基本的背包問題的解題套路。(均基於動態規划的思想)
0-1背包
題目:有 N 件物品和容量為 W 的背包。第 i 件物品的重量為 w_i,價值為 v_i,求將不超過背包容量的物品裝入背包能得到的最大價值。
特點,每件物品的數量只有一個,可以選擇放或不放某件物品。
用dp[i][j]
表示將前 i+1 件總重量不超過 j 的物品放入背包能獲得的最大價值,則可以用以下的轉移方程來表示這個過程:
注意到dp數組第i行的值更新只跟 i-1 行有關,因此可以通過滾動數組或者反向更新的方式優化一下空間復雜度,在動態規划解題的時候這是一種常用的空間復雜度優化方式。優化后的代碼如下:
for(int i = 0; i < N; i++){
// 注意到這里dp需要從后往前更新,避免更新前就把舊值覆蓋
// 從實際意義上來說,因為每件物品只有一個,從后向前更新保證了更新是在還沒放入過當前物品的前提下進行的
for(int j = W; j >= w[i]; j--){
dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
}
}
完全背包
題目:有 N 種物品和容量為 W 的背包。第 i 種物品的重量為 w_i,價值為 v_i,每種物品的數量無限。求將不超過背包容量的物品裝入背包能得到的最大價值。
特點:每種物品的數量無限多。
考慮到每種物品的數量無限。用 dp[j]
表示在重量不超過 j 的情況下背包中物品可以達到的最大價值,則轉移方程如下:
核心代碼如下:
for(int i = 0; i < N; i++){
for(int j = w[i]; j <= W; j++){ // 這里和0-1背包不同
dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
}
}
注意內層for循環是從前向后更新dp數組的,這是唯一和上面的0-1背包問題區別的地方。原因在於,題目中每種物品的數量無限多,在放入一件物品 i 時,要考慮之前已經放過物品 i 的情況。
多重背包
題目:有 N 種物品和容量為 W 的背包。第 i 種物品的數量有n_i個,每個物品重量為 w_i,價值為 v_i,每種物品的數量無限。求將不超過背包容量的物品裝入背包能得到的最大價值。
特點:每種物品的數量不止一個,但有限。
基本的多重背包問題狀態轉移方程如下:
核心代碼如下:
for(int i = 0; i < N; i++){
for(int j = W; j >= w[i]; j--){
for(int k = 1; k <= n[i]; k++){
if(j < k * w[i]) break;
dp[j] = Math.max(dp[j], dp[j - w[i] * k] + v[i] * k);
}
}
}
一些背包問題的題目
上面討論的三種情況只是最基本的背包問題,實際刷題過程中會遇到這些基本問題的變體,例如需要背包正好裝滿、求最小的物品件數、求裝包的方案數等等。這里整理一些題目,后面遇到了持續更新~
0-1背包類題目
考試策略
籃球隊 - 求方案數
牛妹的春游 - 最小花費,多個條件,初始值限制
服務部署 - 多重條件
完全背包類題目
換零錢 - 求總方案數
最少數量貨物裝箱問題 - 物品件數最小,恰好裝滿
拼湊面額 - 求方案數