1 问题引入
01 背包里每种物品只有一件,完全背包里每种物品有无穷多件。中间还有一种情形:每种物品有有限的若干件。
有
样例:
4 10
2 1 3
3 3 2
4 5 1
1 1 4
2 基本思路:直接枚举件数
像完全背包那样,只把「最多选几件」的上界从
比 01 背包多一个
// 多重背包:朴素三重循环
// f[i][j] 前 i 种物品、容量 j 的最大价值,第 i 种可用 m[i] 件
// 时间 O(n * C * sum(m)),直接按「第 i 种选几件」枚举
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e3 + 5;
int n, C;
int w[maxn], v[maxn], m[maxn];
int f[maxn][maxn];
int main() {
cin >> n >> C;
for (int i = 1; i <= n; ++i) cin >> w[i] >> v[i] >> m[i];
for (int i = 1; i <= n; ++i) {
for (int j = 0; j <= C; ++j) {
f[i][j] = f[i - 1][j]; // 第 i 种选 0 件
for (int k = 1; k <= m[i] && k * w[i] <= j; ++k) {
f[i][j] = max(f[i][j], f[i - 1][j - k * w[i]] + k * v[i]);
}
}
}
cout << f[n][C] << endl;
return 0;
}
输出:
11
3 转成 01 背包
最直接的想法是:第
这个想法是对的,但复杂度没变——物品总数还是
4 二进制拆分
关键观察:要能凑出
把
其中
:用 的二进制表示凑; :先取最后那一项 ,剩下的差值落在 内,归结为上一类。
以
于是第
// 多重背包:二进制拆分 + 01 背包
// 把 m 件同种物品拆成系数 1,2,4,...,余数 的若干件,
// 任意 0..m 的取法都能由这些系数之和表示,于是转成 01 背包。
// 时间 O(n * C * log m)
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 5;
int n, C;
int f[maxn];
int iw[maxn * 20], iv[maxn * 20], icnt; // 拆分后的物品
int main() {
cin >> n >> C;
for (int i = 1; i <= n; ++i) {
int w, v, m;
cin >> w >> v >> m;
for (int k = 1; k <= m; k <<= 1) { // 1,2,4,...
iw[++icnt] = k * w; iv[icnt] = k * v;
m -= k;
}
if (m > 0) { iw[++icnt] = m * w; iv[icnt] = m * v; } // 最后一段
}
for (int i = 1; i <= icnt; ++i) { // 01 背包,容量倒序
for (int j = C; j >= iw[i]; --j) {
f[j] = max(f[j], f[j - iw[i]] + iv[i]);
}
}
cout << f[C] << endl;
return 0;
}
输出同样是:
11
5 单调队列优化到 O(nC)
二进制拆分已经把
按容量
设
令
括号里那一项只和
总复杂度
// 多重背包:单调队列优化到 O(nC)
//
// 关键观察:按容量 j 对 w 取模余数 r 分组,同一组里的 j 排成
// r, r+w, r+2w, ...
// 设 j = r + q*w,则转移
// f[i][j] = max{ f[i-1][j-k*w] + k*v } (0 <= k <= m)
// 令 g[q] = f[i-1][r+q*w] - q*v,可化成
// f[i][r+q*w] = q*v + max{ g[p] | q-m <= p <= q }
// 这是滑动窗口最大值,用单调队列 O(1) 均摊求解。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 5;
int n, C;
long long f[maxn], g[maxn];
int q[maxn]; // 单调队列,存下标 p
int main() {
cin >> n >> C;
for (int i = 1; i <= n; ++i) {
int w, v, m;
cin >> w >> v >> m;
if (m > C / w) m = C / w; // 超过容量上限的件数没有意义
for (int r = 0; r < w && r <= C; ++r) {
int cnt = (C - r) / w; // 这一组有 cnt+1 个位置
for (int p = 0; p <= cnt; ++p) {
g[p] = f[r + p * w] - 1LL * p * v;
}
int head = 0, tail = 0; // [head, tail)
for (int p = 0; p <= cnt; ++p) {
// 弹出窗口左端滑出的
while (head < tail && q[head] < p - m) ++head;
// 维护单调递减
while (head < tail && g[q[tail - 1]] <= g[p]) --tail;
q[tail++] = p;
f[r + p * w] = g[q[head]] + 1LL * p * v;
}
}
}
cout << f[C] << endl;
return 0;
}
输出:
11
- 朴素枚举:好写,
,只在 很小时可用。 - 二进制拆分:最常用。
,代码短,几乎不会写错。 - 单调队列:
,最快但最容易写错(下标、窗口边界、 的符号都要小心)。没有性能压力就别上。
6 可行性问题
有一类变体不问最大价值,只问:能否用这些物品恰好凑出某个容量。
此时可以换一种状态设计:设
递推分两步:
- 若
,说明前 种就能凑出 ,那么第 种一件没用, ;否则 。 - 再顺着
从小到大扫一遍:若 ,说明还有余量,可以用一件第 种物品把 也凑出来,于是 。
这样每个状态只被常数次更新,复杂度
7 小结
多重背包是 01 背包和完全背包之间的那一档。核心技巧是二进制拆分:把一个「取 0 到
三种写法的完整代码都在本文的 code/ 目录下,输出已对齐——用同一份输入跑出来都是 11。
8 相关题解
暂无相关题解