dp_背包之多重背包

要求:

看此文需要如下要求。
1,熟悉01背包及完全背包,对01背包/完全背包中的逆序和顺序选择的原因要了解。https://www.cnblogs.com/kingbuffalo/p/16241927.html
2,熟练使用单调队列求滑动窗口中的最大值 https://www.cnblogs.com/kingbuffalo/p/15499998.html

问题:

多重背包也是 0-1 背包的一个变式。与 0-1 背包的区别在于每种物品有ki个,而非一个。

解决方案:

将k个相同的物品,看作k个不同的物品,但是wi,ci都一样。即可套用 01背包方案 详见(https://www.cnblogs.com/kingbuffalo/p/16241927.html)

优化方法:

  1. 二进制优化
    设k个物品分成 A[xx] A[xx+1] ... A[xx+k-1] 个物品。
    那么 A[xx] A[xx+1] 组成两个物品时 与 A[xx+1] A[xx+2] 是相同的,所以存在重复计算。
    所以,不能简单地将其看成k个不同的物品。
    而是考虑二进制 比如将 8 = 1 + 2 + 3 + 1 4个物品,分别为1个的时候,2个的时候,4个的时候,和1个的时候。(这是因为二进制 刚好就是可以表示一切数字,且某位存不存在-->即加不加上)

poj1014的 http://poj.org/problem?id=1014
ac代码如下

#define NMAX 120003

bool dp[NMAX];
int B[10];
int A[1000];


int main(){
	ios::sync_with_stdio(false);
    cin.tie(0);

    int t = 0;
    while(cin >> B[1] >> B[2] >> B[3] >> B[4] >> B[5] >> B[6] ){
    	t ++;
    	int M = B[1] + B[2]*2 + B[3]*3 + B[4]*4 + B[5]*5 +B[6]*6;
    	if ( M == 0 ) break;
    	if ( M & 1 ){
    		cout << "Collection #"<<t <<":\nCan't be divided.\n" << endl;
    	}else{
    		M/=2;
    		int AIdx = 0;
    		for(int i=1;i<=6;i++){
    			int k = B[i];
    			if ( k == 0 ) continue;
          		int j=1;
	    		while(k>=j)A[AIdx++]=j*i,k-=j,j*=2;
	    		if(k)A[AIdx++] = k*i;
    		}
    		memset(dp,0,sizeof(dp));
	    	dp[0] = true;
    		for(int i=0;i<AIdx;++i){
    			for(int j=M;j>=A[i];j--){
    				dp[j] |= dp[j-A[i]];
    			}
    		}
	    	if ( dp[M] ) cout << "Collection #"<<t <<":\nCan be divided.\n" << endl;
	    	else cout << "Collection #"<<t <<":\nCan't be divided.\n" << endl;
    	}
    }
    return 0;
}
  1. 这是一个奇怪的方法,因为它看起来不像背包的算法,这个方法其实比1的方法效率要高
    虽然在poj1014这题上,两种算法耗时都是0Ms
    但在poj1742 http://poj.org/problem?id=1742 上,表示明显。第2种方法需要时间只是第1种的一半。
    但是这个方法有很明显的局限性,只能计算能不能存在的dp,也就是组合成目标(M) 能不能组合成功。
#define NMAX 120003

bool dp[NMAX];
int cnts[NMAX];//保存在dp过程中,使用了多少次此物品
int B[10];

int main(){
	ios::sync_with_stdio(false);
    cin.tie(0);

    int t = 0;
    while(cin >> B[1] >> B[2] >> B[3] >> B[4] >> B[5] >> B[6] ){
    	t ++;
    	int M = B[1] + B[2]*2 + B[3]*3 + B[4]*4 + B[5]*5 +B[6]*6;
    	if ( M == 0 ) break;
    	if ( M & 1 ){
    		cout << "Collection #"<<t <<":\nCan't be divided.\n" << endl;
    	}else{
    		M/=2;
    		memset(dp,0,sizeof(dp));
	    	dp[0] = true;
	    	for(int i=1;i<=6;++i){
	    		memset(cnts,0,sizeof(cnts));
                        //正序是 dp[i]依赖于 <i 的值
                        //正序是 cnts[i] 是 依赖于前面所使用的量
	    		for(int j=i;j<=M;j++){
                                //三个条件,少一不可。 其中 !dp[j] 是非常重要
                                //1. 是为了cnts[j]都正确  
                                //2. 即使是 j=i;j<=M 是顺序的情况下,也可以满足01背包中倒序的需求。(dp[i-1][j] 中[j] 肯定是旧值)
                                //3. 只有在这种dp为bool值的才能这样做,因为如果不是布尔值,无法判断上面提的2种要求
                                //dp[j-i] && cnts[j-i] < B[i] 为了能在数量范围内正确使用
	    			if ( !dp[j] && dp[j-i] && cnts[j-i] < B[i]){
	    				dp[j] = true;
	    				cnts[j] = cnts[j-i]+1;
	    			}
	    		}
	    	}
	    	if ( dp[M] ) cout << "Collection #"<<t <<":\nCan be divided.\n" << endl;
	    	else cout << "Collection #"<<t <<":\nCan't be divided.\n" << endl;
    	}
    }
    return 0;
}
  1. 单调队列优化
    设dp[i][j] 为 考虑第i个物品 能放到j重量的背包时。
    设 w: 重量,v: 价值, m:个数
    因为有m个物品,则
    dp[i][j] = max(dp[i-1][j-w]+v,dp[i-1][j-2w]+2v,...,dp[i-1][j-kw]+kv); 设此式为1式
    而当 j+=w时
    dp[i][j+w] = max(dp[i-1][j]+v,dp[i-1][j-w]+2v,...,dp[i-1][j-(k-1)w]+k*v);设此式为2式
    对比2式与1式,就会发现,这是一个经典的滑动窗口问题。而在滑动窗口内线性查找最大值,是递减单调队列的经典应用
    如图所示(这里用了滚动数组,把i干掉了)

洛谷的 1776题 https://www.luogu.com.cn/problem/P1776

#define WMAX 100005
int dp[WMAX];
int q[WMAX];
int cnt[WMAX];

int main(){
	int n,W;
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin >> n >> W;
	int v,w,m;
	int hh,tt;
	for( int i=0;i<n;++i){
		cin >> v >> w >> m;
		int mMax = min(m,W/w);
		for(int j=0;j<w;j++){//枚举余数
			int seg = W/w;
			hh = 0;tt=-1;
			for( int k=0;k<=seg;++k ){//只有这样的dp[j+x*w] 作为一个循环中,它才能被单调队列优化。
				int dpOldValue = dp[j+k*w] - k*v;//既是滚动数组,又可以顺序,是因为保存在单调队列的值,它是旧的,还没有被更新过的。所以不需要像01背包中那样逆序。
				while(hh<=tt && q[tt]<=dpOldValue) --tt;//递减单调队列保存最大值 比dpOldValue的都推出去
				q[++tt] = dpOldValue;//进队列
				cnt[tt] = k;//这里是要记录进栈的为k,用来记录 单调队列里面数据的下标,以便保证滑动窗口的长度等于nMax个
				while(hh<=tt && cnt[hh]+mMax<k) ++hh;//保证滑动窗口的长度等于nMax个
				dp[j+k*w] = max(dp[j+k*w],q[hh]+k*v);//用最大值来更新dp[j+k*w]
			}
		}
	}
	cout << dp[W] << endl;
	return 0;
}
posted @ 2022-05-19 18:52  传说中的水牛  阅读(44)  评论(0编辑  收藏  举报