多重背包的二进制优化以及单调队列优化
题目概述
给定 \(n\) 个物品和一个容量为 \(m\) 的背包。第 \(i\) 个物品的体积、价值以及数量分别为: \(v_i\)、\(w_i\) 和 \(s_i\),求在总容量不超过 \(m\) 的前提下可获得的最大价值。
- 测试连接:4. 多重背包问题 I
思路
设 \(f_{i,j}\) 表示考虑前 \(i\) 个物品、且在容量约束为 \(j\) 下的最大价值。
由于第 \(i\) 个物品可以选取 \(s_i\) 个,可以类似于为优化的完全背包,在转移时额外增加一层循环枚举选取数量不超过 \(s_i\) 的情况,其时间复杂度为\(O(nms)\),其中 \(s=\max\{s_1,\dots,s_n\}\)。当 \(n,m,s\) 同阶时,可写作:\(O(n^3)\)。下面直接讨论优化方式。
二进制优化
完全背包的优化方式不能直接应用于多重背包。因为完全背包允许无限取用某一物品,其状态转移所依赖的状态大于等于多重背包。下面通过一个简单例子说明这一差异:
- 考虑 \(n=1,m=9,v_1=3,w_1=5,c_1=2\),可以发现,若为完全背包,最大价值为 15;而在多重背包中,最大价值仅为 10。
换句话说,多重背包在将第 \(i\) 件物品全部选取完后,仍然存在剩余容量,从而导致其性质与完全背包本质不同。
引理(二进制拆分原理)
若 \(S=2^0+2^1+\dots+2^{k-1}\),则集合 \(\{2^0,2^1,\dots,2^{k-1}\}\) 的元素可以通过加法表示区间 \([0,S]\) 内的所有整数。
推论
进一步证明推论:若 \(S=2^0+2^1+\dots+2^{k-1}+x, \ 1\le x<2^{k}\),则集合 \(\{2^0,2^1,\dots,2^{k-1},x\}\) 可以组合出 \([0,S]\) 内的所有整数。
设集合 \(Q=\{2^0,2^1,\dots,2^{k-1}\}\),并令 \(S'=2^0+2^1+\dots+2^{k-1}\)。由上述引理可知,区间 \([0,S']\) 可以由集合 \(Q\) 表示。于是只需证明区间 \([S',S]\) 可以由集合 \(P=Q\cup\{x\}\)表示。
对于任意 \(S'+n,1\le n\le x\),有
又因为 \(n\le x\Rightarrow S'+n-x\le S'\),所以 \(S'+n-x\) 可由集合 \(Q\) 表示,进而 \(S'+n\) 可由集合 \(P\) 表示,故命题成立。
回到多重背包问题,对于第 \(i\) 个物品的数量 \(s_i\),若可以将其拆分为若干份,使这些份数的能够表示区间 \([0,s_i]\) 内的任意整数。具体而言,将 \(s_i\) 按 2 的整数幂进行拆分。设 \(k\) 为满足 \(s_i\ge2^0+2^1+\dots+2^{k-1}\) 的最大整数,分类讨论 \(s_i\) 的两种情况:
- 若 \(s_i=2^0+2^1+\dots+2^{k-1}\),由二进制拆分定理,其可被转换为 01 背包问题
- 若 \(s_i=2^0+2^1+\dots+2^{k-1}+x,1\le x<2^{k}\),由上述推论,同样可以转换为 01 背包问题。
通过上述分析,多重背包问题可以通过二进制拆分完全转化为 01 背包问题,下面给出若干示例以帮助理解拆分过程:
- \(6=1+2+3\)
- \(8=1+2+4+1\)
- \(18=1+2+4+3\)
- \(31=1+2+4+8+16\)
二进制优化参考代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int v[N], w[N];
int f[N];
int main() {
int n, m, cnt = 0;
cin >> n >> m;
for (int i = 1; i <= n; i ++) {
int a, b, s, k = 1;
cin >> a >> b >> s;
while (s >= k) {
v[++ cnt] = k * a;
w[cnt] = k * b;
s -= k;
k *= 2;
}
if (s) {
v[++ cnt] = s * a;
w[cnt] = s * b;
}
}
n = cnt;
for (int i = 1; i <= n; i ++) {
for (int j = m; j >= v[i]; j --) {
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[m];
return 0;
}
复杂度分析
- 时间复杂度:\(O(m \sum \log s_i)\)。每个物品 \(s_i\) 被拆分为 \(O(\log s_i)\) 个新物品,每个新物品做一次 \(O(m)\) 的 01 背包转移。
- 空间复杂度:\(O(m + \sum \log s_i)\)。
单调队列优化
仔细观察状态转移表达式
其中第 \(i\) 件物品的体积为 \(v\),价值为 \(w\),数量上限为 \(s\)。
不难发现,\(f_{i,j}\) 仅可能由满足 \(j\equiv k\pmod v,\ k\le j\) 的 \(f_{i-1,k}\) 转移而来。换句话说,对于当前重量 \(j\) 而言,其只依赖于 \(i-1\) 阶段中 \(\bmod v\) 同余的更小的体积 \(k\)。
举个例子:设 \(v=2,j=9\),其依赖的状态分别 \(f_{i-1,9},f_{i-1,7},f_{i-1,5},f_{i-1,3},f_{i-1,1}\),均满足\(k\equiv9\pmod2\)。
因此,可以对所有可能的容量 \(j\) 按照模 \(v\) 分组,枚举余数 \(r=j\bmod v, \ r\in\{0,1,\dots,v-1\}\),对于每个余数 \(r\),枚举以 \(r\) 为起点,\(v\) 为步长的容量序列
其中 \(k\) 为满足 \(r+kv\le m\) 的最大整数。
固定 \(r\),设当前容量为 \(j=r+kv\),根据上述分析,其应从 \(i-1\) 阶段的 \([j-sv,j-(s-1)v, \dots,j-v,j]\) 中选择使得 \(f_{i,j}\) 最大的容量,这属于分组窗口最大值问题,可以用单调队列优化。
需要声明的是,这里队列存储的不是最大容量,而是使得后续状态最优的容量。
定义队列 \(q\) 维护 \(i-1\) 阶段在模 \(v\) 意义下的大小为 \(s + 1\) 的窗口 \([j-sv,j]\) 中,使得 \(f_{i,j}\) 最优的状态对应的容量,其中 \(h, t\) 分别表示 \(q\) 的头尾索引。
考虑如何维护队列最大值。设后续容量为 \(x\),则当
时,则说明 \(f_{i-1,j}\) 的作为 \(f_{i,x}\) 的转移状态比 \(f_{i-1,q_{t}}\) 更优,且更靠后,于是移除队尾。但由于 \(x\) 是未知量,于是将上述不等式的 \(x\) 右移,得到
,用该不等式作为维护队列单调性的判断条件。
然后将 \(j\) 加入队列,并判断是否 \(q_{t}\) 在窗口 \([j - sv,j]\) 内部,即若 \(q_{h}<j-sv\),则将队头出队。
接着用 \(q_{h}\) 更新 \(f_{i,j}\),即
参考代码 1
#include <bits/stdc++.h>
using namespace std;
const int N = 1010, M = 2000;
int f[N][M], q[M];
int main() {
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i ++) {
int v, w, s;
cin >> v >> w >> s;
for (int r = 0; r < v; r ++) {
int h = 0, t = -1;
for (int j = r; j <= m; j += v) {
while (h <= t && f[i - 1][j] >= f[i - 1][q[t]] + (j - q[t]) / v * w) {
t --;
}
q[++ t] = j;
if (h <= t && q[h] < j - s * v) h ++;
f[i][j] = max(f[i][j], f[i - 1][q[h]] + (j - q[h]) / v * w);
}
}
}
cout << f[n][m];
return 0;
}
空间优化
不难发现,与 01 背包类似,\(i\) 阶段仅仅依赖于 \(i-1\) 阶段,但不同的是,由于多重背包是在枚举 \(i\) 阶段的在模 \(v\) 意义下的容量 \(j\) 的同时通过队列维护 \(i-1\) 阶段的在模 \(v\) 意义下的容量 \([j-sv,j]\),若倒序枚举,则无法用队列维护这个 \(k\)。
于是我们考虑用一个数组 \(g\) 维护上一个阶段的容量,于是
但需要注意的是,\(f_{j}\) 在未被更新前,本身表示的就是原 \(f_{i-1,j}\) 的状态,所以队列维护的窗口应该变为 \([j-sv,j-v]\)。而这只需要将维护队列最大值与更新状态进行互换即可。
参考代码 2
#include <bits/stdc++.h>
using namespace std;
const int N = 20010;
int f[N], g[N], q[N];
int main() {
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i ++) {
int v, w, s;
cin >> v >> w >> s;
memcpy(g, f, sizeof(g));
for (int r = 0; r < v; r ++) {
int h = 0, t = -1;
for (int j = r; j <= m; j += v) {
if (h <= t && q[h] < j - s * v) h ++;
if (h <= t) f[j] = max(f[j], g[q[h]] + (j - q[h]) / v * w);
while (h <= t && g[j] >= g[q[t]] + (j - q[t]) / v * w) t --;
q[++ t] = j;
}
}
}
cout << f[m];
return 0;
}
复杂度分析
- 时间复杂度:\(O(nm)\)。
- 空间复杂度:\(O(m)\)。

浙公网安备 33010602011771号