动态规划(背包九讲 二)

3.多重背包问题

1.题意

有N 种物品和一个容量为V 的背包。第i 种物品最多有 s[i] 件可用,每件体积是v[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

2.思路

这题目和完全背包问题很类似。因为对于第i 种物品有s[i]+1种策略:取0 件,取1 件……取p [ i ] 件。令dp [ i ] [ j ] 表示前i 种物品恰放入一个容量为j 的背包的最大价值,则有状态转移方程:

dp[i] [j]=max(dp[i−1] [jkv[i]]+kw[i])|0≤ks[i]

#include<bits/stdc++.h>
using namespace std;
int v[105],w[105],s[105];
int dp[105];
int main(){
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>v[i]>>w[i]>>s[i];
	}
	for(int i=1;i<=n;i++){
		for(int j=m;j>=v[i];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]);
			}
		}
	}
	cout<<dp[m];
}

但是这样写的话时间复杂度会很高O(n*m *s[i]),如果数据为2000就会超时,所以要进行优化。

3.代码优化

在进行优化前先讲一个小例子:

假设有 50个苹果,现在要取n个苹果,朴素的做法是将苹果一个一个拿出来,直到取出n个。再假设有 50个苹果和 6个箱子,利用箱子进行一些预备工作,可以在每个箱子中放 2^k个苹果。也就是1,2,4,8,16,19(剩余的数),取任意n个苹果时,只要推出几只箱子就可以了,例如n=3,我们只需推出前两个物品。可以证明对于任意的n,这种组合方式总是存在的,是不是更加的简便。

二进制拆分思想:将第 i种物品拆分成若干件物品,每件物品的体积和价值乘以一个拆分系数(1,21,22…2^(k-1),s[i] -2^k+1(剩余的数)),就可以转化为 01背包的物品求解。例如,s[i] =12,拆分系数为1,2,4,5,转化为 4件 01背包的物品:(v[i],w[i]),(2v[i],2w[i]),(4v[i],4w[i]),(5v[i],5w[i])。所以各组相互组合包含了0~s[i]的所有情况。

#include<bits/stdc++.h>
using namespace std;
const int N=25000; //2的12次方大于2000,也就是一个数可以拆分12个 
int vv[N],ww[N],ss[N]; //所以数组容量乘以12 
int dp[2001];
int main(){
	int n,m;
	cin>>n>>m;
	int w,v,s,t=0;
	for(int i=1;i<=n;i++){
		cin>>v>>w>>s;
		for(int j=1;j<=s;j<<=1){ //二进制拆分 
			vv[++t]=j*v; //存体积 
			ww[t]=j*w; //存容量 
			s-=j; //物品件数减去拆分的
		}
		if(s){ 
			vv[++t]=s*v; //如果数量还有剩余 
			ww[t]=s*w;
		}
	}
	for(int i=1;i<=t;i++){ // 01背包 
		for(int j=m;j>=vv[i];j--){
			dp[j]=max(dp[j],dp[j-vv[i]]+ww[i]);
		} 
	}
	cout<<dp[m];
} 

方法二:

int num = min(s, m / v); // m/v是最多可以放几个,优化上界
		for (int k = 1; num > 0; k <<= 1) { //这里的k就相当于上面例子中的1,2,4,6
			if (k > num) k = num;
			num -= k;
			for (int j = m; j >= v * k; j--) // 01背包
				dp[j] = max(dp[j], dp[j - v * k] + w * k);
		}

优化后的代码可以解决多重背包2,下面有b站的讲解视频,供大家查阅

二进制优化

4.混合背包

1.题意

如果将前面三个背包混合起来,也就是说,有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包),应该怎么求解呢?

2.思路

我们可以对不同的背包用不同的方法计算,故如果只有两类物品:一类物品只能取一次,另一类物品可以取无限次,那么只需在对每个物品应用转移方程时,根据物品的类别选用顺序或逆序的循环即可,复杂度是Θ ( V ∗ N ) ,最后再加上多重背包,多重背包要用二进制优化,不然会超时。在做多重背包的问题时尽量用二进制优化

#include<bits/stdc++.h>
using namespace std;
int dp[1005];
int main(){
	int n,m;
	int v,w,s;
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>v>>w>>s;
		if(s==0){
			for(int j=v;j<=m;j++)
			dp[j]=max(dp[j],dp[j-v]+w);
		}
		else if(s==-1){
			for(int j=m;j>=v;j--)
			dp[j]=max(dp[j],dp[j-v]+w);
		}
		else{
			int num=min(s,m/v);
			for(int k=1;num>0;k<<=1){
				if(k>num) k=num;
				num-=k;
				for(int j=m;j>=v*k;j--)
				dp[j]=max(dp[j],dp[j-v*k]+w*k);
			}
	    }
    }
	cout<<dp[m];
}

5.二维费用背包

1.题目

二维费用的背包问题是指:对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设第i 件物品所需的两种代价分别为c [ i ] 和g [ i ] 。两种代价可付出的最大值(两种背包容量)分别为V 和M。物品的价值为w[i]。

2.思路

和 01背包一样,费用加了一维,只需状态也加一维即可。设dp[i ] [j ] [k] 表示前i 件物品付出两种代价分别最大为j 和k 时可获得的最大价值。状态转移方程就是:

dp[i] [j] [k]=max(dp[i−1] [j] [k],dp[i−1] [jv] [km]+w])

#include<bits/stdc++.h>
using namespace std;
int dp[1005][105][105];
int main(){
	int m1,n,v1;
	cin>>n>>v1>>m1;
	int v,w,m;
	for(int i=1;i<=n;i++){
		cin>>v>>m>>w;
		for(int j=1;j<=v1;j++){
			for(int k=1;k<=m1;k++){
				dp[i][j][k]=dp[i-1][j][k];
				if(j>=v&&k>=m)
				dp[i][j][k]=max(dp[i-1][j][k],dp[i-1][j-v][k-m]+w);
			}
		}
	}
	cout<<dp[n][v1][m1]; 
}

如前述方法,可以只使用二维的数组:当每件物品只可以取一次时变量j 和k 采用逆序的循环,当物品有如完全背包问题时采用顺序的循环。当物品有如多重背包问题时可拆分物品。最终答案为dp [ V ] [ M ],代码:

#include<bits/stdc++.h>
using namespace std;
int dp[105][105];
int main(){
	int m1,n,v1;
	cin>>n>>v1>>m1;
	int v,w,m;
	for(int i=1;i<=n;i++){
		cin>>v>>m>>w;
		for(int j=v1;j>=v;j--){
			for(int k=m1;k>=m;k--){
				dp[j][k]=max(dp[j][k],dp[j-v][k-m]+w);
			}
		}
	}
	cout<<dp[v1][m1]; 
}

物品总个数的限制

有时,“二维费用”的条件是以这样一种隐含的方式给出的:最多只能取M 件物品。这事实上相当于每件物品多了一种“件数”的费用,每个物品的件数费用均为1 ,可以付出的最大件数费用为M 。换句话说,设dp [ i ] [ j ] 表示付出费用i 、最多选j 件时可得到的最大价值,则根据物品的类型(01、完全、多重)用不同的方法循环更新。

6.分组背包问题

1.题意

有N 件物品和一个容量为V 的背包。第i 件物品的体积是v[ i ] ,价值是w [ i ] 。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

2.思路

和 01背包类似,只不过进行了分组,那么对于每一组的物品有若干种策略:是选择本组的某一件,还是一件都不选。

也就是说明dp [ k ] [ j ] [j]表示前k 组物品花费费用j 能取得的最大价值,则有:

dp [k] [j]=max(dp[k−1] [j],dp[k−1] [jv[i]]+w[i]|物品i⊆组k)

#include<bits/stdc++.h>
using namespace std;
int dp[10005],v[105],w[105];
int main(){
	int m,n;
	cin>>n>>m;
	int s;
	for(int i=1;i<=n;i++){
		cin>>s;
		for(int j=1;j<=s;j++){
			cin>>v[j]>>w[j];
		}  // 01背包体积从后往前遍历,防止答案被覆盖
		for(int j=m;j>=0;j--) //因为每组物品只能选一个,
		for(int k=1;k<=s;k++){ //所以可以覆盖之前组内物品最优解的来取最大值
			if(j>=v[k])
			dp[j]=max(dp[j],dp[j-v[k]]+w[k]);
		}
	}
	cout<<dp[m];
}

与多重背包的关系

分组背包在面对一组内s ss个的物品时,共有s + 1 种决策情况,分别是选第0 , 1 , 2… s 个物品(选第0个物品即不选该组内任何物品)。多重背包中每个物品有s 个,也有s + 1种决策情况,分别是选0 , 1 , 2… s 个该物品,在此可以和上面的情况做一下对比区分。多重背包可以说是分组背包的一个特殊情况,所以多重背包可以用放弃数组完整性的代价来优化算法。

小结

分组的背包问题将彼此互斥的若干物品称为一个组,这建立了一个很好的模型。不少背包问题的变形都可以转化为分组的背包问题(例如有依赖的背包),由分组的背包问题进一步可定义“泛化物品”的概念,十分有利于解题。

7.背包问题求方案数

1.题意

和 01背包一样,就是求当背包达到最优解时,有几种不同的选择方案。

2.思路

我们可以用两个数组一个数组dp[i] 代表前i件物品恰放入一个容量为j的背包可以获得的最大价值,另一个数组c[i]代表前i 件物品体积为j 时,价值最大的方案数

1.如果第 i个物品我们可以选择,且总价值增大,则更新do[j]和c[j]。**背包容量从 j-v更新到 j,只是多装入一个物品,没有改变方案数,所以c[j]=c[j-v]。**状态就由c[j] 转移到了c[j-v]。

2.若第 i个物品装入背包,且总价值没有增加,那么总价值就不用再更新了,对于方案数c 那就有两种情况:

  • 若不装入该物品,容量已有的方案数为c[j];

  • 若装入该物品,容量 j对应的方案数为c[j-v]; 我们放入该物品时的价值不放入该物品时的价值相同说明这是两种 不同的情况,对于这两种情况我们我们求和,就是当体积为j 时,价值最大的方案数。

3.如果不装入第 i个物品,那么容量 j 已有的方案数为 c[j]。

在这里插入图片描述

下面的 f 数组代表dp数组,左边有 >代表是选当前物品价值大于不选的时候,= 就是相等的时候。

在这里插入图片描述

#include<bits/stdc++.h>
using namespace std;
#define ll long long 
ll mod=1e9+7;
ll dp[1005],c[1005]; //最大价值,方案数
int main(){
	int n,m;
	cin>>n>>m;
	int v,w;
	for(int i=0;i<=m;i++)
	c[i]=1;
	for(int i=1;i<=n;i++){
		cin>>v>>w;
		for(int j=m;j>=v;j--){
			if(dp[j-v]+w>dp[j]){ //如果选当前物品比不选的价值更大的话,那么就更新dp[j]的值
				dp[j]=dp[j-v]+w; //更新方案数为c[j-v]
				c[j]=c[j-v]%mod;
			}
			else if(dp[j]==dp[j-v]+w) //如果选和不选的价值相等的话,那么这就是两种情况
			c[j]=(c[j]+c[j-v])%mod;  //两种情况的方案数相加
		}
	}
	cout<<c[m];
}

8.背包问题求具体方案数

1.题意

和求方案数的题一样,只不过这个要输出具体的选择方案,输出字典序最小的。

例题

2.思路

假设存在包含第1 个物品的最优解,为了确保字典序最小那么我们必然要选第一个。那么问题就为从2~n这些物品中找到最优解。

首先我们从后向前遍历物品,让最优解落在dp[1] [m]中。

状态定义:dp[i] [j]代表从第i 个物品到最后一个物品装入容量为j 的背包最大价值(01背包为前i 个,这个是从第i 个到第n 个)

状态转移:dp[i] [j]=max(dp[i+1] [j],dp[i+1] [j-v[i]]+w[i])

不选第i 个物品:dp[i] [j]=dp[i+1] [j]

选第i 个物品:dp[i] [j]=dp[i+1] [j-v[i]]+w[i]

下面的 f 数组就代表dp 数组

在这里插入图片描述

#include<bits/stdc++.h>
using namespace std;
int dp[1005][1005];
int v[1005],w[1005];
int main(){
	int m,n;
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>v[i]>>w[i];
	} 
	for(int i=n;i>=1;i--){  //逆序取物 
		for(int j=0;j<=m;j++){
			dp[i][j]=dp[i+1][j]; //先赋值上一层的值 
			if(j>=v[i])
			dp[i][j]=max(dp[i][j],dp[i+1][j-v[i]]+w[i]);
		}
	}
	int j=m; //剩余容量 
	for(int i=1;i<=n;i++){ //找路,从1开始是否满足这个等式
		if(j>=v[i]&&dp[i][j]==dp[i+1][j-v[i]]+w[i]){ 
			cout<<i<<' '; //如果满足这个等式说明一定选择了这个这个物品,就输出
			j-=v[i]; //选了第 i个物品,剩余容量减小 
		}
	} 
} 

下面有一个选择的过程:

在这里插入图片描述

视频讲解

参考博客

总结

先循环物品再循环体积再循环决策,01背包体积逆序,完全背包体积正序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值