目录
题目解析
这是一道典型的多重背包问题。
问题描述:给定
朴素的动态规划做法是三重循环:枚举物品种类
这种做法的时间复杂度是
下面分别介绍两种常见的优化方法:二进制拆分优化和单调队列优化。
方法一:二进制拆分优化 (Binary Splitting Optimization)
1. 原理解析
多重背包问题的瓶颈在于物品数量的限制。如果我们能把第
如何拆分最高效?利用二进制的思想。任何一个正整数
例如,若某物品有 13 件,我们可以拆分成数量为 1, 2, 4, 6 的四组新物品。
- 第 1 组:重量
,价值 - 第 2 组:重量
,价值 - 第 3 组:重量
,价值 - 第 4 组:重量
,价值
通过组合这四组新物品(选或不选),我们可以凑出
2. 复杂度分析
对于每种物品
对于题目数据,
转化后的 0/1 背包时间复杂度为
3. C++ 代码实现
/**
* Author by Rainboy blog: https://rainboylv.com github : https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* date: 2026-01-05 08:48:02
*/
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 定义新物品结构体
struct Goods {
int v, w;
};
int n, W;
// dp数组,大小需要超过最大载重
int dp[40005];
vector<Goods> goods_list;
int main() {
// 加速输入输出
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> W;
// 1. 二进制拆分过程
for (int i = 0; i < n; ++i) {
int v, w, m;
cin >> v >> w >> m;
// 将m拆分为 1, 2, 4, ...
for (int k = 1; k <= m; k <<= 1) {
goods_list.push_back({k * v, k * w});
m -= k;
}
// 处理剩下的余数 R
if (m > 0) {
goods_list.push_back({m * v, m * w});
}
}
// 2. 0/1 背包过程
for (const auto& g : goods_list) {
// 0/1 背包内层循环倒序
for (int j = W; j >= g.w; --j) {
dp[j] = max(dp[j], dp[j - g.w] + g.v);
}
}
cout << dp[W] << endl;
return 0;
}
方法二:单调队列优化 (Monotonic Queue Optimization)
1. 原理解析
让我们回到朴素的状态转移方程:
观察这个方程,当我们在计算
我们可以按照容量
我们将方程中的
令
现在核心看大括号里的部分。对于一个固定的余数
我们需要维护一个单调递减队列,队列中存储的是商的下标
2. 复杂度分析
-
外层循环枚举物品种类:
次。 -
中层循环枚举余数:
次。 -
内层循环枚举商:
次。 -
单调队列操作是均摊
的。 总时间复杂度 =
。 代入数据
,这个复杂度非常优秀,是多重背包问题的最优解法之一。
3. C++ 代码实现
为了方便理解,使用两个数组 dp 和 pre_dp 来分别表示当前层和上一层的状态。也可以通过使用一维数组和临时变量来实现滚动数组优化空间。
#include <iostream>
#include <vector>
#include <deque>
#include <algorithm>
using namespace std;
int n, W;
int v[105], w[105], m[105];
// dp[j] 表示容量为 j 时的最大价值
int dp[40005];
// pre_dp 用于存储上一轮物品处理后的状态,用于辅助单调队列计算
int pre_dp[40005];
// 计算单调队列中用于比较的值
inline int calc_val(int idx, int weight, int value, int r) {
return pre_dp[idx * weight + r] - idx * value;
}
int main() {
// 加速输入输出
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> W;
for (int i = 1; i <= n; ++i) {
cin >> v[i] >> w[i] >> m[i];
}
// 初始化 dp 数组为 0
fill(dp, dp + W + 1, 0);
// 枚举每一种物品
for (int i = 1; i <= n; ++i) {
// 备份上一轮的状态
copy(dp, dp + W + 1, pre_dp);
// 枚举余数 r
for (int r = 0; r < w[i]; ++r) {
deque<int> dq; // 单调队列存储的是商的下标 q
// 枚举商 q,对应的容量是 j = q * w[i] + r
for (int q = 0; q * w[i] + r <= W; ++q) {
int current_capacity = q * w[i] + r;
int current_val_for_compare = calc_val(q, w[i], v[i], r);
// 1. 入队操作:维护队列单调递减性
// 如果队尾元素对应的值小于等于当前值,则队尾元素永远不会成为最大值,弹出
while (!dq.empty() && calc_val(dq.back(), w[i], v[i], r) <= current_val_for_compare) {
dq.pop_back();
}
dq.push_back(q);
// 2. 出队操作:检查队头是否滑出窗口范围
// 窗口大小为 m[i]+1,有效范围是 [q - m[i], q]
if (dq.front() < q - m[i]) {
dq.pop_front();
}
// 3. 更新 DP 状态
// 队头保存的是窗口内的最优 k'
int best_k_prime = dq.front();
// 代入公式:dp[j] = (pre_dp[k'*w+r] - k'*v) + q*v
// 化简后为:dp[j] = pre_dp[k'*w+r] + (q - k') * v
// 其中 (q - k') 就是选择当前物品的数量
dp[current_capacity] = pre_dp[best_k_prime * w[i] + r] + (q - best_k_prime) * v[i];
}
}
}
cout << dp[W] << endl;
return 0;
}
总结
对于多重背包问题:
- 数据范围较小时,可以直接用朴素的三重循环
。 - 一般数据范围(如本题,
中等, 较大),推荐使用二进制拆分优化,转化为 0/1 背包,复杂度 ,实现简单,不易出错。 - 对时间要求极高或数据范围更大时,使用单调队列优化,复杂度
,是理论上的最优复杂度,但实现细节较多,推导公式需要仔细。