最近在牛客刷题遇到好几道背包问题,索性这两天集中火力刷了一些这类的题。这里总结一下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背包类题目
考试策略
篮球队 - 求方案数
牛妹的春游 - 最小花费,多个条件,初始值限制
服务部署 - 多重条件
完全背包类题目
换零钱 - 求总方案数
最少数量货物装箱问题 - 物品件数最小,恰好装满
拼凑面额 - 求方案数