多重背包及优化

版权声明:Powered By Fighter https://blog.csdn.net/qq_30115697/article/details/87465924

多重背包及优化

题目描述

​ 有 n n 个物品和一个容量为 m m 的背包,其中每种物品的数量是有限的,第 i i 种物品的体积,价值,数量分别记为 w [ i ] w[i] v [ i ] v[i] c [ i ] c[i] 。求能取到的最大或最小价值


解决方案

1. 暴力出奇迹

​ 很显然,我们可以把多个同一种物品拆成不同的物品,相当于把多重背包强行转化为01背包,此时物品数量由 n n 变为了 i = 1 n c [ i ] \sum\limits_{i=1}^nc[i] , 那么复杂度变为了 O ( m i = 1 n c [ i ] ) O(m*\sum\limits_{i=1}^n c[i]) ,显然这个算法十分愚蠢傻逼

​ 虽然比较愚蠢,但实际上后面的优化都是在这个基本的思想上进行优化。

2.二进制拆分优化

​ 暴力的方法愚蠢在哪里呢?因为把一种物品全部拆开后数量太多了,而且可以发现要组成取一种物品的所有情况并不需要全部拆开,比如我想取两个同种物品,但我却会遍历每种取法,尽管这些取法是完全相同的。所以我们就想到,有没有一种方法可以用更少的物品数表示出所有情况呢?很显然,我们以二进制的形式拆分一个种类,比如说一种物品有11个,于是我们可以拆分为1,2,4,剩下5单独一组,这样就可以表示出所有情况。而这样就把每种物品拆分的数量优化到了 l o g 2 c [ i ] log_2c[i] ,于是整个算法的时间复杂度就优化到了 m i = 1 n l o g 2 c [ i ] m*\sum\limits_{i=1}^nlog_2c[i]

核心代码(拆分)

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];
    }
}

其中 x x z z 数组分别是读入的原始 w w v v 数组,而 y y 数组则是原始 c c 数组。

3. 单调队列优化

也不知道哪个神人想出来了这么一个神奇的算法。

​ 这里给出一个与暴力解法等价的转移方程,只不过我们用第 i i 种物品来表示当前总体积 m m 。于是:
f [ i ] [ j w [ i ] + m o d ] = m a x ( f [ i 1 ] [ j w [ i ] + m o d ] , f [ i 1 ] [ ( j k ) w [ i ] + m o d ] + k v [ i ] ) f[i][j*w[i]+mod]=max(f[i-1][j*w[i]+mod],f[i-1][(j-k)*w[i]+mod]+k*v[i])
​ 其中 0 m o d &lt; w [ i ] , j = m / w [ i ] , 0 k j 0 \leq mod &lt; w[i], j=m/w[i], 0\leq k \leq j ,而我们在枚举背包体积时,也不再使用m~0的枚举顺序,而是使用枚举 m o d mod j j 组合起来表示体积。这会使后面的单调队列优化变得容易很多。 注意,这里的 j j 不一定要小于等于 c [ i ] c[i] ,因为这里的 j j 并不是第 i i 种物品的个数,而只是用 w [ i ] w[i] 作为基数表示出了体积,真正对 c [ i ] c[i] 的控制会在单调队列中体现。

​ 接下来就来考虑优化,从上面这个方程中可以发现,不管一种物品取几个,原式中的 m o d mod 是不会改变的,于是我们可以把每种物品按余数分类,而状态之间的转移仍然只会在同一类中进行,而此时每一类中的状态满足单调性且均为连续的,每一个状态的转移相当于在它之前找一个连续区间中的最值,于是可以用单调队列进行优化。

​ 设单调队列为 q q ,头指针为 l l ,尾指针为 r r ,那么队首出队的条件为 j q [ l ] &gt; c [ i ] j-q[l]&gt;c[i] ,即控制每种物品最多选 c [ i ] c[i] 个,在插入队列时判断 f [ i 1 ] [ q [ l ] w [ i ] + m o d ] + ( j q [ l ] ) v [ i ] f[i-1][q[l]*w[i]+mod]+(j-q[l])*v[i] f [ i 1 ] [ j w [ i ] + m o d ] f[i-1][j*w[i]+mod] 的关系,维护单调递增或递减即可。

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,达到空间上的优化。

当然,如果觉得二维的 f f 数组看着不顺眼,可以强行转化为一维,但好像要另开空间存储上一次状态的数据(我并不确定是否有更优的方案),对空间几乎没有优化。


例题

洛谷P3423 Banknotes

物品价值恒为1,且要求价值最小值的多重背包,还要输出方案。

这道题loj上还有一个弱化版,不需要输出方案。

对于输出方案,我们可以用单调队列优化,并记录 l a s t [ i ] [ j ] last[i][j] 表示状态 f [ i ] [ j ] f[i][j] 是由 f [ i 1 ] [ l a s t [ i ] [ j ] ] f[i-1][last[i][j]] 转移而来,然后递归输出方案。当然二进制优化也可以,只是方案更难得到。

代码

#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;
}

猜你喜欢

转载自blog.csdn.net/qq_30115697/article/details/87465924