目录
概述
AcWing 4|5|6:
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。输入格式
第一行两个整数,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 ,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N≤10000
0<V≤200000
0<vi,wi,si≤20000输入样例
4 5 1 2 3 2 4 1 3 4 3 4 5 2输出样例:
10
多重背包是背包问题中较为复杂的一个问题,我们将依次介绍朴素算法、二进制优化、单调队列优化,来依次解决AcWing 4、5、6。
之所以称为多重背包,是因为每个物品有选择次数限制。
思路
朴素地来讲,我们用三重循环来解决这个问题。
定义dp[a][b]为能够物品选择范围为前a个物品且当前背包最多能装体积为b的物品总量时的最优解。
这样,我们就可以从小问题推导出大问题的解,应该是这样的:
考虑选择范围为前i个物品,体积为j,当dp[i - 1]的所有状态已知时,对于第i个物品,有选几个的状态,取所有情况最大值。
即dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i] * k] + w[i] * k) (1 <= k <= s[i]);
如果 j < v[i],则代表放不下这个物品,dp[i][j] = dp[i - 1][j];
算法过程
我们可以得到非常直观的三重循环。
我们直接做一次空间优化,即使用一维dp数组。
这一部分可以参照此前的背包dp文章。
Code
#include <bits/stdc++.h>
using namespace std;
constexpr int N = 101;
int n, m;
int v[N], w[N], s[N];
int dp[N];
int solve(){
for (int i = 1; i <= n; i++)
for (int j = m; j; j--) {
for (int k = 1; k <= s[i] && k * v[i] <= j; k++)
dp[j] = max(dp[j], dp[j - k * v[i]] + k * w[i]);
}
return dp[m];
}
int main(){
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> v[i] >> w[i] >> s[i];
cout << solve();
return 0;
}
复杂度
时间复杂度: O(m
si)
空间复杂度: O(m)
可惜这个时间复杂度过高,仅能通过AcWing 4。
优化方案
多重背包有着两个非常知名的优化。
1.二进制优化
证明一件事:
将自然数a进行2进制拆分为 a = + m,
即序列1,2,4,...,2^n,m,其中0 <= m < 2^(n + 1)。
这个序列的部分元素相加可以组合出[1, a]之间的所有自然数。
证明:{
由等比数列求和:1+2+⋯+2^(n)=2^(n + 1)−1。
- 对于[1, 2^(n + 1) - 1]自然数:
考虑二进制01表示:0001,0010,0100,1000的组合可以表示1111内的所有数。
- 对于(2^(n + 1) - 1, a]自然数:
设有(2^(n + 1) - 1, a]内的自然数 b = t + m,其中 a = 2^(n + 1) − 1 + m。
b <= a,即 t <= 2^(n + 1) − 1。
若t不等于0,那么t属于[1, 2^(n + 1) - 1]]即第一种情况,可以被表示;若t = 0,b = m。
组合出t后想组合出b还剩一个m,这可以被a序列中的m表示。
}
那么我们可以对k件相同物品进行二进制拆分,这个二进制序列可以组成1到k件原始物品,那就是转化为了对这个二进制序列的01背包进行求解。
Code
#include <bits/stdc++.h>
using namespace std;
constexpr int N = 1e5 + 5;
int n, m, new_n = 1;
int v[N], w[N];
int dp[N];
int solve(){
for (int i = 1; i < new_n; i++)
for (int j = m; j >= v[i]; j--)
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
return dp[m];
}
int main(){
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int vv, ww, s;
cin >> vv >> ww >> s;
int idx = 1;
while(s >= idx) {
v[new_n] = vv * idx;
w[new_n] = ww * idx;
new_n++;
s -= idx;
idx *= 2;
}
if (s){
v[new_n] = vv * s;
w[new_n] = ww * s;
new_n++;
}
}
cout << solve();
return 0;
}
复杂度
时间复杂度: O(m
logsi)
空间复杂度: O(m)
可惜这个时间复杂度还是不够低,不能通过AcWing 6。
2.单调队列优化
换个思路。
考虑当前第 i 个物品。
观察转移方程:dp[j] = max(dp[j], dp[j - k * v[i]] + k * w[i]);
当前dp[j]总是从dp[j - v[i]], dp[j - 2 * v[i]],....,中转移得到,而这些下标的共同点是对v[i]同余。(就是指 j % v[i] == (j - v[i]) % v[i],这里很好理解)
也就是说状态是在同余下标内部转移的,那我们就可以把所有的下标按对v[i]的余数分组,然后分组循环。
更进一步,考虑我们的转移规律:从同余元素中挑选此前的最大价值进行转移,我们可以考虑单调队列:「队列」单调队列 / LeetCode 239(C++)
具体的想法是这样的:
由于物品个数有限,队列的大小是有限的,不能超过物品数目,也就是不能从非法状态转移:
若s[i] = 2, dp[j] 不能从 dp[j - 3 * v[i]] + 3 * w[i]转移,因为3超过s[i]了。
用队头储存可行的最有价值的下标,用以转移到当前 j 处。
所谓容量a比b有价值,就是指在 a 大于 b的情况下,b的价值加上b扩容至a的额外容量下增添的物品 i 价值仍然打不过a,即dp[a] >= dp[b] + (a - b) / v[i] * w[i]。
形象理解:
b a j
[vi] [vi] [vi] [vi]|
若dp[j]来自a,则dp[a] + 2 * vi > dp[b] + 3 * vi。
以这个逻辑维护单调队列,即若 j 比队尾更优,则队尾弹出,直到 j 不再更优,将 j 加入队尾。
Code
#include <bits/stdc++.h>
using namespace std;
constexpr int N = 2e4 + 5;
int n, m;
int v[N], w[N], s[N];
int dp[N], assist[N], q[N];
int solve(){
for (int i = 1; i <= n; i++){
copy(dp, dp + m + 1, assist);
for (int k = 0; k < v[i]; k++) {
int h = 0, t = 0;
q[t] = k;
for (int j = k + v[i]; j <= m; j += v[i]) {
if (j - q[h] > s[i] * v[i])
h++;
dp[j] = max(assist[j], assist[q[h]] + (j - q[h]) / v[i] * w[i]);
while (h <= t && assist[j] - assist[q[t]] >= (j - q[t]) / v[i] * w[i])
t--;
q[++t] = j;
}
}
}
return dp[m];
}
int main(){
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> v[i] >> w[i] >> s[i];
cout << solve();
return 0;
}
在细节处理上,由于单调队列从左向右滑动,倒序枚举思路已不可行,我们使用两个一维数组来优化空间,用assist辅助数组存储上次计算的结果。
j < v[i] 时,表示一个物品 i 都无法放入背包,我们只需要将其入队,然后计算从 k + v[i]开始的j。
std::deque常数过大,在复杂度正确的情况下也会爆掉,所以用原生数组模拟队列。
复杂度
时间复杂度: O(nm)
空间复杂度: O(m)
复杂度分析:
时间分析:{
最外层 i 枚举n次,
对于每个 i 枚举 k 分为v[i]组。
对于每个 k 枚举 j 有m / v[i]次循环,单调队列内m / v[i]个元素各进队出队一次,均摊到 j 为O(1)。
O(n * v[i] * m / v[i]) = O(nm)。
}
总结
多重背包问题是背包系列中较复杂的一个问题,希望你有所收货。。
278



被折叠的 条评论
为什么被折叠?



