今天是我们讲解「动态规划专题」中的 「背包问题」的第八篇。
今天我们将学习第三种背包问题:多重背包。
另外,我在文章结尾处列举了我所整理的关于背包问题的相关题目。
背包问题我会按照编排好的顺序进行讲解(每隔几天更新一篇,确保大家消化)。
你也先可以尝试做做,也欢迎你向我留言补充,你觉得与背包相关的 DP 类型题目 ~
有
种物品和一个容量为
的背包,每种物品「数量有限」。
第
件物品的体积是
,价值是
,数量为
。
问选择哪些物品,每件物品选择多少件,可使得总价值最大。
其实就是在 0-1 背包问题的基础上,增加了每件物品可以选择「有限次数」的特点(在容量允许的情况下)。
示例 1:
输入: N = 2, C = 5, v = [1,2], w = [1,2], s = [2,1]
输出: 4
解释: 选两件物品 1,再选一件物品 2,可使价值最大。
在之前的章节我们说过,几乎所有的「背包问题」都是基于「01 背包」演变而来。
因此,当我们确定一个问题是背包问题之后,可以根据其物品的「数量限制」来判别是何种背包问题,然后套用「01 背包」的思路来求解。
具体的,我们可以套用「01 背包」的「状态定义」来进行分析:
代表考虑前
件物品,且所选物品总体积不超过
时获得的最大价值。
由于每件物品可以被选择「有限次」,因此对于某个
而言,其值应该为以下所有可能方案中的最大值:
件物品
的最大价值,即
件物品
的最大价值,即
件物品
的最大价值,即
...
件物品
的最大价值,
由此我们可以得出「状态转移方程」为:
可以发现其状态转移方程与 完全背包 完全一致,只是多了
的条件。
也好理解,毕竟「完全背包」不限制物品数量,「多重背包」限制物品数量。
代码:
class Solution {
public int maxValue(int N, int C, int[] s, int[] v, int[] w) {
int[][] dp = new int[N][C + 1];
// 先处理第一件物品
for (int j = 0; j <= C; j++) {
// 显然当只有一件物品的时候,在容量允许的情况下,能选多少件就选多少件(不超过限制数量)
int maxK = Math.min(j / v[0], s[0]);
dp[0][j] = maxK * w[0];
}
// 处理剩余物品
for (int i = 1; i < N; i++) {
for (int j = 0; j <= C; j++) {
// 不考虑第 i 件物品的情况
int n = dp[i- 1][j];
// 考虑第 i 件物品的情况
int y = 0;
for (int k = 1; k <= s[i]; k++) {
if (j < k * v[i]) {
break;
}
y = Math.max(y, dp[i - 1][j - k * v[i]] + k * w[i]);
}
dp[i][j] = Math.max(n, y);
}
}
return dp[N - 1][C];
}
}
,其中
。整体复杂度为
通过观察我们的「状态转移方程」可以发现,我们在更新某个
的时候只依赖于
。
因此我们可以像 01 背包那样使用「滚动数组」的方式将空间优化到
。
代码:
class Solution {
public int maxValue(int N, int C, int[] s, int[] v, int[] w) {
int[][] dp = new int[2][C + 1];
// 先处理第一件物品
for (int j = 0; j <= C; j++) {
// 显然当只有一件物品的时候,在容量允许的情况下,能选多少件就选多少件(不超过限制数量)
int maxK = Math.min(j / v[0], s[0]);
dp[0][j] = maxK * w[0];
}
// 处理剩余物品
for (int i = 1; i < N; i++) {
for (int j = 0; j <= C; j++) {
// 不考虑第 i 件物品的情况
int n = dp[(i- 1)&1][j];
// 考虑第 i 件物品的情况
int y = 0;
for (int k = 1; k <= s[i]; k++) {
if (j < k * v[i]) {
break;
}
y = Math.max(y, dp[(i - 1)&1][j - k * v[i]] + k * w[i]);
}
dp[i&1][j] = Math.max(n, y);
}
}
return dp[(N - 1)&1][C];
}
}
,其中
。整体复杂度为
在之前的「01 背包」和「完全背包」都可以进行「一维空间优化」。
其中「完全背包」的「一维空间优化」还能有效降低时间复杂度。
那么「多重背包」是否也能通过「一维空间优化」来降低时间复杂度呢?
答案是可以优化空间,但是不能降低时间复杂度。
因为当我们像「完全背包」那样只保留「容量维度」,并且「从小到大」遍历容量的话,我们在转移
时是无法直接知道所依赖的
到底使用了多少件物品
的。
这个问题在「完全背包」里面无须关心,因为每件物品可以被选择无限次,而在「多重背包」则是不能忽略,否则可能会违背物品件数有限的条件。
因此,「多重背包」问题的「一维空间优化」并不能像「完全背包」那样使复杂度降低。
代码:
class Solution {
public int maxValue(int N, int C, int[] s, int[] v, int[] w) {
int[] dp = new int[C + 1];
for (int i = 0; i < N; i++) {
for (int j = C; j >= v[i]; j--) {
for (int k = 0; k <= s[i] && j >= k * v[i]; k++) {
dp[j] = Math.max(dp[j], dp[j - k * v[i]] + k * w[i]);
}
}
}
return dp[C];
}
}
,其中
。整体复杂度为
至此,三类传统背包问题的「一维空间优化」方式都已经讲过了。
我们发现,只有「完全背包」的「一维空间优化」是存在数学意义上的优化(能够有效降低算法时间复杂度)。
「01 背包」和「多重背包」的「一维空间优化」其实只是基于「朴素二维」解法做单纯的「滚动」操作而已(利用状态之间的依赖关系,配合遍历顺序,使得不再需要参与转移的空间能够被重新利用)。
因此,一定程度上,可以将「多重背包」看做是一种特殊的「01 背包」。
对「01 背包」中具有相同的价值 & 成本的物品进行计数,就成了对应物品的「限制件数」,「01 背包」也就转换成了「多重背包」。
同理,将「多重背包」的多件物品进行「扁平化展开」,就转换成了「01 背包」。
扁平化需要遍历所有物品,枚举每件物品的数量,将其添加到一个新的物品列表里。
再套用「01 背包」的解决方案。
代码:
class Solution {
public int maxValue(int N, int C, int[] s, int[] v, int[] w) {
// 将多件数量的同一物品进行「扁平化」展开,以 [v, w] 形式存储
List<int[]> arr = new ArrayList<>();
for (int i = 0; i < N; i++) {
int cnt = s[i];
while (cnt-- > 0) {
arr.add(new int[]{v[i], w[i]});
}
}
// 使用「01 背包」进行求解
int[] dp = new int[C + 1];
for (int i = 0; i < arr.size(); i++) {
int vi = arr.get(i)[0], wi = arr.get(i)[1];
for (int j = C; j >= vi; j--) {
dp[j] = Math.max(dp[j], dp[j - vi] + wi);
}
}
return dp[C];
}
}
。共有
个状态需要被转移,复杂度为
,因此整体复杂度为
可以发现,转换成「01 背包」之后,时间复杂度并没有发生变化。
因此将「多重背包」简单的转换成「01 背包」并不能带来效率的提升。
甚至说转换成「01 背包」之后效率上还要稍微差一点,因为额外增大了“常数”。
我们来可以分析一下为什么。
首先,扁平化操作并没有使得物品“变少”,我们仍然需要枚举所有的“组合”,从中选择最优,组合的数量没有发生变化,还额外增加了扁平化的操作。
这是将「多重背包」转换成「01 背包」进行求解没有“实际意义”的原因。
直接的转换并不能带来效率上的提升,但是可以让我们更加了解两者之间的关系。
今天我们学习了【动态规划/背包问题】中的「多重背包」问题。
无论是「朴素二维」、「滚动数组」、「一维优化」还是「扁平化」都不能优化「多重背包」问题的时间复杂度。
在各维度数量级同阶的情况下,时间复杂度是
的。
这意味着我们只能解决
数量级的「多重背包」问题。
同时,我们能总结出:在传统的三种背包问题的「一维空间优化」里,只有「完全背包」的「容量维度」是「从小到大」的,其他两种背包的「容量维度」都是「从大到小」的。
下一节,我们将会讲解如何降低「多重背包」问题的时间复杂度。
这是我们「刷穿 LeetCode」系列文章的第 No.*
篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先将所有不带锁的题目刷完。
在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。
为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode。
在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。