多重背包及优化
文章目录
题目描述
有 个物品和一个容量为 的背包,其中每种物品的数量是有限的,第 种物品的体积,价值,数量分别记为 , , 。求能取到的最大或最小价值
解决方案
1. 暴力出奇迹
很显然,我们可以把多个同一种物品拆成不同的物品,相当于把多重背包强行转化为01背包,此时物品数量由
变为了
, 那么复杂度变为了
,显然这个算法十分愚蠢傻逼。
虽然比较愚蠢,但实际上后面的优化都是在这个基本的思想上进行优化。
2.二进制拆分优化
暴力的方法愚蠢在哪里呢?因为把一种物品全部拆开后数量太多了,而且可以发现要组成取一种物品的所有情况并不需要全部拆开,比如我想取两个同种物品,但我却会遍历每种取法,尽管这些取法是完全相同的。所以我们就想到,有没有一种方法可以用更少的物品数表示出所有情况呢?很显然,我们以二进制的形式拆分一个种类,比如说一种物品有11个,于是我们可以拆分为1,2,4,剩下5单独一组,这样就可以表示出所有情况。而这样就把每种物品拆分的数量优化到了 ,于是整个算法的时间复杂度就优化到了 。
核心代码(拆分)
for(int i = 1; i <= n; i++){
for(int j = 1; j <= y[i]; j <<= 1){
cnt++;
w[cnt] = j*x[i];
v[cnt] = j*z[i];
y[i] -= j;
}
if(y[i] > 0){
cnt++;
w[cnt] = y[i]*x[i];
v[cnt] = y[i];
}
}
其中 和 数组分别是读入的原始 和 数组,而 数组则是原始 数组。
3. 单调队列优化
也不知道哪个神人想出来了这么一个神奇的算法。
这里给出一个与暴力解法等价的转移方程,只不过我们用第
种物品来表示当前总体积
。于是:
其中
,而我们在枚举背包体积时,也不再使用m~0的枚举顺序,而是使用枚举
和
组合起来表示体积。这会使后面的单调队列优化变得容易很多。 注意,这里的
不一定要小于等于
,因为这里的
并不是第
种物品的个数,而只是用
作为基数表示出了体积,真正对
的控制会在单调队列中体现。
接下来就来考虑优化,从上面这个方程中可以发现,不管一种物品取几个,原式中的 是不会改变的,于是我们可以把每种物品按余数分类,而状态之间的转移仍然只会在同一类中进行,而此时每一类中的状态满足单调性且均为连续的,每一个状态的转移相当于在它之前找一个连续区间中的最值,于是可以用单调队列进行优化。
设单调队列为 ,头指针为 ,尾指针为 ,那么队首出队的条件为 ,即控制每种物品最多选 个,在插入队列时判断 和 的关系,维护单调递增或递减即可。
Tip:在实际运行过程中,由于常数过大,单调队列优化的效果很可能不如二进制优化
核心代码
for(int i = 1; i <= n; i++){
for(int mod = 0; mod < w[i]; mod++){
l = 1, r = 0;
q[l] = 0;
for(int j = 0; j*w[i]+mod <= m; j++){
while(l <= r && j-q[l] > c[i]){
l++;
}
f[i][j*w[i]+mod] = min(f[i-1][q[l]*w[i]+mod]+(j-q[l])*v[i], f[i-1][j*w[i]+mod]);
while(l <= r && f[i-1][q[l]*w[i]+mod]+(j-q[l])*v[i] >= f[i-1][j*w[i]+mod]){
r--;
}
q[++r] = j;
}
}
}
同时,还可以使用滚动数组,用i&1和i&1^1代替i和i-1,达到空间上的优化。
当然,如果觉得二维的 数组看着不顺眼,可以强行转化为一维,但好像要另开空间存储上一次状态的数据(我并不确定是否有更优的方案),对空间几乎没有优化。
例题
洛谷P3423 Banknotes
物品价值恒为1,且要求价值最小值的多重背包,还要输出方案。
这道题loj上还有一个弱化版,不需要输出方案。
对于输出方案,我们可以用单调队列优化,并记录 表示状态 是由 转移而来,然后递归输出方案。当然二进制优化也可以,只是方案更难得到。
代码
#include <bits/stdc++.h>
using namespace std;
int n, m;
int w[205], c[205], f[2][20005];
int q[20005], l, r;
int main()
{
cin >> n;
for(int i = 1; i <= n; i++){
scanf("%d", &w[i]);
}
for(int i = 1; i <= n; i++){
scanf("%d", &c[i]);
}
cin >> m;
memset(f, 0x3f, sizeof(f));
f[0][0] = 0;
for(int i = 1; i <= n; i++){
for(int mod = 0; mod < w[i]; mod++){
l = 1, r = 0;
q[l] = 0;
for(int j = 0; j*w[i]+mod <= m; j++){
while(l <= r && j-q[l] > c[i]){
l++;
}
int now = i&1, last = now^1;
f[now][j*w[i]+mod] = min(f[last][q[l]*w[i]+mod]+j-q[l], f[last][j*w[i]+mod]);
while(l <= r && f[last][q[l]*w[i]+mod]+j-q[l] >= f[last][j*w[i]+mod]){
r--;
}
q[++r] = j;
}
}
}
cout << f[n&1][m] << endl;
return 0;
}