多重背包
多重背包
一、问题描述
有 种物品和一个容量为 的背包。第 种物品的体积为 ,价值为 ,最多可以选择 件。
在所选物品总体积不超过 的前提下,求能够得到的最大总价值。
本节沿用《完全背包》的符号,允许背包没有装满,也允许一件物品都不选。
三种背包问题的区别在于每种物品可以选择的数量:
| 问题 | 第 种物品可以选择的件数 |
|---|---|
| 01 背包 | 或 |
| 完全背包 | ,直到容量不足 |
| 多重背包 | ,同时满足容量限制 |
本节代码约定 、、、、。
二、二进制优化:把物品拆成若干组
1. 转化为 01 背包
将 件相同物品逐件作为 01 背包物品,数量可能过多。二进制优化将它们按 件分组,最后不足下一组的部分单独作为一组。
每组视为一件新的 01 背包物品,只能整组选或不选。 拆出的组需要能表示选择 到 件的所有情况。
如果某组包含 件第 种物品,这件新物品的属性为:
$$\text{体积}=c\cdot w[i],\qquad \text{价值}=c\cdot v[i].$$2. 拆分示例
假设一种物品的体积为 ,价值为 ,最多可以选 件。
拆分的要求是:各组最多选一次,拼出的件数恰好覆盖 到 的所有整数,既不能遗漏,也不能超过 。
依次拆出 件,总共拆出了 件,还剩 件。不足下一组的 件,因此将剩余 件作为最后一组:
得到七件新的 01 背包物品:
| 新物品 | 包含原物品的件数 | 总体积 | 总价值 |
|---|---|---|---|
| A | 1 | 2 | 3 |
| B | 2 | 4 | 6 |
| C | 4 | 8 | 12 |
| D | 8 | 16 | 24 |
| E | 16 | 32 | 48 |
| F | 32 | 64 | 96 |
| G | 37 | 74 | 111 |
例如选择 件,可以选取 这几组。原来需要处理 件物品,现在只需处理 组。
3. 为什么能够表示所有件数?
前六组 可以按二进制组合出 到 的所有整数。因此:
- 不选最后的 件这一组,可以拼出 中的所有整数。
- 选择这组,再搭配前六组,可以拼出 中的所有整数。
两段区间合起来恰好是 ,所以所有件数都能拼出。全部组的件数之和为 ,也不会超出上限。
一般数量的拆分同理:前面的二次幂组覆盖一段连续区间,最后的余数组不会造成空缺,因此保留了原问题的所有合法件数。
注意,不能只按 的二进制表示拆成 ,这样虽然总和正确,却无法拼出 、 等数量。
三、拆分后进行 01 背包转移
设 dp[j] 表示处理过的组中,总体积不超过 时能够得到的最大总价值。
因为允许一件物品都不选,初始时 dp[] 全部为 。
对于包含 件原物品的一组,令 weight = c * w、value = 1LL * c * v,进行 01 背包转移:
for (int j = V; j >= weight; --j) {
dp[j] = max(dp[j], dp[j - weight] + value);
}
容量必须倒序枚举。 原物品虽然可以选多件,但每一组只能选一次。如果改成正序,同一组就可能被重复选择,导致超出原物品的数量上限。
可以一边拆分一边更新 dp[],不需要把所有新物品存下来。
四、参考代码
输入第一行为 ,接下来 行,每行输入一种物品的体积、价值和数量上限,即 。输出最大总价值。
背包最多装下 件同种物品,因此拆分前先令 s = min(s, V / w),减少无用的组。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 5005;
int n, V;
long long dp[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> V;
for (int i = 1; i <= n; ++i) {
int w, v, s;
cin >> w >> v >> s;
// 超过容量允许的件数没有必要考虑
s = min(s, V / w);
// 依次拆出 1、2、4……件,最后一组取剩余件数
for (int k = 1; s > 0; k *= 2) {
int c = min(k, s);
int weight = c * w;
long long value = 1LL * c * v;
// 当前这一组最多选择一次,按照 01 背包倒序更新
for (int j = V; j >= weight; --j) {
dp[j] = max(dp[j], dp[j - weight] + value);
}
s -= c;
}
}
cout << dp[V] << '\n';
return 0;
}
价值乘法和 DP 状态使用 long long,因为最大总价值可能达到 。
五、复杂度
设有效数量为:
$$t_i=\min\left(s[i],\left\lfloor\frac{V}{w[i]}\right\rfloor\right).$$第 种物品拆成 组,每一组进行一次 的 01 背包转移。因此,时间复杂度为:
其中 为读入开销。有效数量不超过 ,因此时间上界也可写为 。
边读入边拆分、边进行转移,只使用一个 DP 数组,空间复杂度为 。
六、单调队列优化
多重背包还可以使用单调队列进一步优化到 。单调队列优化多重背包的方法见 DP 优化章节。