一、 问题分析
多重背包的本质是在 0-1 背包的基础上增加了数量限制。其状态转移方程为:
其中
。
两种算法优化:
1. 二进制拆分优化 (Binary Splitting)
- 数学原理:任何正整数
都可以唯一地表示为若干个 2 的幂次项之和与一个余项之和。例如
。通过这种拆分,我们可以将
个物品组合成
组“捆绑包”。
- 实现简单,且能将多重背包转化为 0-1 背包。其理论上限为
。
2. 单调队列优化 (Monotonic Queue Optimization)
- 数学原理:观察状态转移方程,对于固定的余数
,状态转移仅在
这一序列中进行。通过变量替换,可以将原方程改写为滑动窗口最值问题。
- 理论最优:其复杂度为
,与
无关,在
极大时具有绝对优势。
二、 代码实现
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
array<ll, 3005> dp;
array<ll, 3005> g;
void solve() {
int n, m;
cin >> n >> m;
dp.fill(0);
ll base = 0;
deque<int> dq;
for (int i = 0; i < n; i++) {
int w, v, s;
cin >> w >> v >> s;
if (w == 0) {
base += (ll)v * s;
continue;
}
g = dp;
// 遍历余数 r,将背包容量划分为 w 个等差序列
for (int r = 0; r < w; r++) {
dq.clear();
// 序列中的第 k 个元素对应容量 j = r + k * w
for (int k = 0; r + k * w <= m; k++) {
int j = r + k * w;
// 计算当前状态的特征值,用于单调队列比较
// 目标:维护 g[r + t*w] - t*v 的单调递减队列
ll curVal = g[j] - (ll)k * v;
// 1. 维护队列单调性:如果当前值比队尾更优,弹出队尾
while (!dq.empty()) {
ll backVal = g[r + dq.back() * w] - (ll)dq.back() * v;
if (backVal <= curVal) {
dq.pop_back();
} else {
break;
}
}
dq.push_back(k);
// 2. 维护窗口大小:如果队首下标超出数量限制 s,弹出队首
if (dq.front() < k - s) {
dq.pop_front();
}
// 3. 更新当前状态 dp[j]
// 方程:dp[k*w+r] = max(g[t*w+r] + (k-t)*v)
int best_t = dq.front();
dp[j] = g[r + best_t* w] + (ll)(k - best_t) * v;
}
}
}
cout << dp[m] + base << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
solve();
}
return 0;
}
三、 复杂度分析
| 算法 | 时间复杂度 | 空间复杂度 | 评价 |
|---|---|---|---|
| 朴素 DP | 无法通过,在本题中 |
||
| 二进制拆分 | 复杂度稍高。 | ||
| 单调队列 | 最优解。复杂度与物体数量 |
Trade-offs (权衡):
- 二进制拆分的优势在于代码逻辑复用(复用 0-1 背包逻辑),但如果
进一步增大或
变得更苛刻,则会失效。
- 单调队列虽然增加了实现复杂度(需要维护 Deque),但它将多重背包问题在时间曲线上拉平到了与完全背包/0-1 背包同级的效率。

京公网安备 11010502036488号