#6562. 2026/4/26/周日下午笔记(多重背包)
2026/4/26/周日下午笔记(多重背包)
📦 多重背包(Multiple Knapsack)
一、问题模型
有 N 种物品和容量为 V 的背包。第 i 种物品最多有 sᵢ 件,每件体积 vᵢ,价值 wᵢ。
求体积总和 ≤ V 时的最大价值。
三种背包核心区别:
| 类型 | 可选次数 | 内层循环方向 | 转移关键 |
|---|---|---|---|
| 01 背包 | 1 次 | 倒序 | dp[i-1][j-v[i]] |
| 完全背包 | 无限次 | 正序 | dp[i][j-v[i]] |
| 多重背包 | 最多 sᵢ 次 | 见下文 | 需要枚举个数 |
二、二维写法(朴素)
状态定义
dp[i][j]:考虑前 i 种物品,背包容量为 j 时的最大价值。
转移方程
不选第 i 种物品:
dp[i][j] = dp[i-1][j]
选第 i 种物品的 k 个(k = 1, 2, ..., sᵢ):
dp[i][j] = max(dp[i][j], dp[i-1][j - k*v[i]] + k*w[i])
(需满足 j ≥ k × v[i])
代码
#include <bits/stdc++.h>
using namespace std;
int n, m;
int v[110], w[110], s[110];
int dp[110][110];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> v[i] >> w[i] >> s[i];
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++) {
dp[i][j] = dp[i - 1][j]; // 不选
for (int k = 1; k <= s[i]; k++) { // 选 k 个
if (j >= k * v[i])
dp[i][j] = max(dp[i][j],
dp[i - 1][j - k * v[i]] + k * w[i]);
}
}
}
cout << dp[n][m] << endl;
return 0;
}
复杂度:O(N × V × Σsᵢ)
适用范围:N, V, s ≤ 100
三、一维写法(朴素,滚动数组)
优化思路
dp[i][j] 只依赖 dp[i-1][...],可压缩为一维。体积必须倒序枚举,防止同一件物品被重复使用。
代码
#include <bits/stdc++.h>
using namespace std;
int n, m;
int dp[1010];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int v, w, s;
cin >> v >> w >> s;
for (int j = m; j >= v; j--) // 倒序!
for (int k = 1; k <= s; k++) // 枚举选几个
if (j >= k * v)
dp[j] = max(dp[j], dp[j - k * v] + k * w);
}
cout << dp[m] << endl;
return 0;
}
问题:内层有三重循环,当 s 很大时会超时 → 需要优化。
四、二进制优化 ⭐ 最常用
核心思想
将 sᵢ 件物品用 2 的幂次 拆分,使枚举次数从 O(sᵢ) 降到 O(log sᵢ)。
拆分方法
以 s = 10 为例:
10 = 1 + 2 + 4 + 3
↑
剩余部分单独一组
拆成 4 个"打包物品":拿 1 件、拿 2 件、拿 4 件、拿 3 件。
通过这 4 个组合,可以凑出 1~10 的任意数量。
拆分模板
int k = 1;
while (k <= s) {
// 打包 k 个体积为 v、价值为 w 的物品
items.push_back({k * v, k * w});
s -= k;
k *= 2;
}
if (s > 0)
items.push_back({s * v, s * w});
完整代码
#include <bits/stdc++.h>
using namespace std;
int n, m;
int dp[2010];
struct Item {
int v, w; // 打包后的体积、价值
};
int main() {
cin >> n >> m;
vector<Item> items;
// 二进制拆分
for (int i = 1; i <= n; i++) {
int v, w, s;
cin >> v >> w >> s;
int k = 1;
while (k <= s) {
items.push_back({k * v, k * w});
s -= k;
k *= 2;
}
if (s > 0)
items.push_back({s * v, s * w});
}
// 01 背包(每个打包物品只能选一次)
for (auto &item : items)
for (int j = m; j >= item.v; j--)
dp[j] = max(dp[j], dp[j - item.v] + item.w);
cout << dp[m] << endl;
return 0;
}
复杂度:O(N × V × log Σsᵢ)
适用范围:N ≤ 1000, V ≤ 2000, s ≤ 2000
五、三种解法对比
| 方法 | 时间复杂度 | 适用场景 |
|---|---|---|
| 二维朴素 | O(NVΣs) | 数据小,理解原理 |
| 一维朴素 | 数据小,节省空间 | |
| 二进制优化 | O(NV·log Σs) | 最常用,推荐 |
六、典型例题
例题(模板·多重背包)
输入:
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出:
10
解释:
- 物品 1:体积 1,价值 2,最多 3 件
- 物品 2:体积 2,价值 4,最多 1 件
- 物品 3:体积 3,价值 4,最多 3 件
- 物品 4:体积 4,价值 5,最多 2 件
最优方案:物品 1 取 2 件(v=2, w=4)+ 物品 2 取 1 件(v=2, w=4)+ 物品 3 取 0 件 + 物品 4 取 0 件 → 总体积 4,总价值 8;或物品 2 + 物品 3 = 体积 5,价值 8。
七、与混合背包的关系
多重背包是混合背包的一部分:
| s 值 | 背包类型 | 循环方向 |
|---|---|---|
| s = -1 | 01 背包 | 倒序 |
| s = 0 | 完全背包 | 正序 |
| s > 0 | 多重背包 | 二进制拆分后倒序 |
八、记忆口诀
多重物品有上限,二进拆分最擅长;
一拆 log 件物品,01 背包一遍过。