【动态规划】背包问题(01背包,完全背包,多重度背包)

对于背包问题用文字很难描述清楚也很难理解,所以本篇仅描述基本思想框架,仅供学习参考!!!


背包问题:给定一组物品,每种物品有自己的重量和价格,在限定的重量内,我们如何选择,才能使物品的总价格最高。

三种背包

  • 01背包:每个物品只能选择一次
  • 完全背包:每个物品选择次数不限制
  • 多重度背包:每个物品只能选择限定次数

一、01背包

问题描述:有 n件物品和一个容量是 m 的背包。每件物品只能使用一次。第 i件物品的体积是 w[i],价值是 c[i]。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。

基本实现
背包问题思想的核心在于‘“选择第i个物品/不选择第i个物品

常设以下变量
i:第i个物品
j:当前背包容量为j
m:表示背包容量
n:表示物品数量
w[i]:表示第i个物品的重量
c[i]:表示第i个物品的价值
dp[i][j]:表示前i个物品,背包容量为j时,得到的最大价值

情况一(不拿第i个物品,背包当前容量为j时)
这种情况第i个物品不拿,那么就只能在第i-1个物品中拿,而此时背包的容量没有变,依然是j
方程:dp[i][j]=d[i-1][j]

情况二 (拿第i个物品,背包当前容量为j时)
这情况当拿了第i个物品之后,背包剩余容量为j-w[i],所以拿了第i个物品之后要在剩余的i-1个物品中将剩余的j-w[i]容量装满
方程:dp[i][j]=d[i-1][j-w[i]]+c[i]

背包的思想是:在每次选择中只需要考虑拿与不拿,比较这两种情况哪一种收益最大,也就是情况一和情况二谁的dp[i][j]的值最大
状态转移方程dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+c[i])

代码演示:

#include<iostream>
#include<algorithm>
using namespace std;
int n,m,w[5005],c[5005],dp[5005][5005];
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){//w[0]和c[0]都初始化为0 
		cin>>w[i]>>c[i];
	}
	//背包
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			//不拿第i个物品 
			dp[i][j]=dp[i-1][j];
			 
			if(w[i]<=j){//背包容量大于第i个物品容量才有拿的可能 
				dp[i][j]=max(dp[i][j],dp[i-1][j-w[i]]+c[i]);//选择两种情况的最大价值 
			}
		}
	} 
	cout<<dp[n][m]<<endl;//输出物品数目为n,背包容量为m的最大价值 
	return 0;
} 

一维优化(优化空间复杂度)

观察上面代码过程可以发现,我们处理dp[i][j]时只与i和i-1有关,dp[i][j]只与当前行和前一行有关,所以其实可以优化为滚动数组来实现,也就是将二维数组压缩成一维,减少了空间复杂度
注意:在01背包中,滚动数组需要逆向处理
在这里插入图片描述
状态转移方程dp[j]=max(dp[j],dp[j-w[i]]+c[i])(j表示哦当前背包大小)

逆向处理
逆向处理滚动数组的精髓在于,如果没有动过这个位置的数,这个位置的数就会保留dp[i-1][j](二维数组的上一行)的旧值,若动过才会更新覆盖新值dp[i][j](二维数组的当前行),所以在进行比较的时候,保证了被比较的旧值不会发生改变,更新时,更新后的新值也不会再发生改变,只比较更新一次,就是01背包;

实现了同时对dp[i-1][j]dp[i][j]的值可以用一维数组进行比较覆盖更新

示例
我们假设此时i=5;w[5]=2,c[5]=2,j=6
对于dp[j]=max(dp[j],dp[j-w[i]]+c[i])也就是dp[6]=max(dp[6],dp[6-2]+2)
括号内的dp[6]就是dp[4][6]继承下来的值(也就是不选第i个物品的情况)
括号内的dp[6-2]+2=dp[4]+2,dp[4]就是dp[4][4]继承下来的值(也就是选的情况)
发现dp[6]=5,dp[6-2]+2=6,后者(选第i个物品的情况)值比较大,即dp[6]更新为dp[6]=6

代码演示:

#include<iostream>
#include<algorithm>
using namespace std;
int n,m,w[5005],c[5005],dp[5005]; 
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>w[i]>>c[i];
	}
	for(int i=1;i<=n;i++){
		for(int j=m;j>=w[i];j--){//当j<w[i]时没有选择的机会,一个小优化 
			dp[j]=max(dp[j],dp[j-w[i]]+c[i]);
		}
	}
	cout<<dp[m]<<endl;
	return 0;
} 

变量优化(优化空间复杂度)

在上面的代码中,w[i]和c[i]其实都只使用了一次,所以对于这两个数组完全可以省略,用变量来代替

演示代码:

#include<iostream>
#include<algorithm>
using namespace std;
int n,m,w,c,dp[5005]; 
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
			cin>>w>>c;
		for(int j=m;j>=w;j--){//当j<w[i]时没有选择的机会,一个小优化 
			dp[j]=max(dp[j],dp[j-w]+c);
		}
	}
	cout<<dp[m]<<endl;
	return 0;
} 

二、完全背包

问题描述:有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。第 i 种物品的体积是 w[i],价值是 c[i]。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。

朴素算法

朴素方法我们只需在01背包代码上加一个循环就可以实现,题目说每个物品可以无限可用,那只需要引入一个for(int k=0;k<=j/w[i];k++)循环,表示放入k个物品求dp[j]最大值,但是k不能大于j/w[i],要不然会超出背包容量,还有相应的重量价值都需要乘k

演示代码:

#include<iostream>
#include<algorithm>
using namespace std;
int n,m,w[50005],c[50005],dp[50005]; 
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>w[i]>>c[i];
	}
	for(int i=1;i<=n;i++){
		for(int j=m;j>=w[i];j--){
			for(int k=0;k<=j/w[i];k++) 
			dp[j]=max(dp[j],dp[j-w[i]*k]+c[i]*k);
		}
	}
	cout<<dp[m]<<endl;
	return 0;
} 

此代码时间复杂度过大,不宜使用!!!

经典算法(优化时间复杂度)

同样是选择与不选择
不选择:dp[i][j]=dp[i-1][j]
选择:dp[i][j]=dp[i][j-w[i]]

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

可以看出,完全背包在选择的情况下,之后并不是在i-1个物品里拿物品,而是可以继续在i个物品里面拿物品,用同一行的数计算更新,就实现了物品可以多次选择

那么如何理解这个多次呢?
假设当前有A,B二件物品的重量价值都为1,手动模拟第一行如下(注意可以重复选择):
在这里插入图片描述处理到第二行,也就是物品B的选择与不选择讨论

当j=1时

dp[2][1]=max(dp[1][1],dp[2][1-1]+1)=max(1,1)

选择与不选择结果一样,假设没有选择
在这里插入图片描述
当j=2时

dp[2][2]=max(dp[1][2],dp[2][2-1]+1)=max(2,2)

选择与不选择结果一样假设选择了B
在这里插入图片描述
当j=3时

dp[2][3]=max(dp[2-1][3],dp[2][3-1]+1)=max(2,3)

可见此时选择B得到最大值为3
在这里插入图片描述
到这里会惊奇得发现,dp[2][3]的价值由{A,B,B}得来,出现了重复选择,这就是完全背包的奥秘,可以实现重复选择
代码示例:

#include<iostream>
#include<algorithm>
using namespace std;
int n,m,w[50005],c[10005],dp[10005][10005];
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>w[i]>>c[i];
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			dp[i][j]=dp[i-1][j];//不拿
			if(j>=w[i]){//判断是否可以拿 
				dp[i][j]=max(dp[i][j],dp[i][j-w[i]]+c[i]);
			} 
				
		}
	}
	cout<<dp[n][m];
	return 0;
} 

一维优化(优化空间复杂度):

二维完全背包同样可以优化成一维数组,减少空间复杂度
优化方法:顺向处理一维数组即可
顺向处理可以实现重复选择
具体结合01背包的逆向处理类比即可
代码示例:

#include<iostream>
#include<algorithm>
using namespace std;
int n,m,w[10005],c[10005],dp[10005];
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>w[i]>>c[i];
	}
	for(int i=1;i<=n;i++){
		for(int j=w[i];j<=m;j++){//j<w[i]没有选择的机会 
			dp[j]=max(dp[j],dp[j-w[i]]+c[i]);
		}
	}
	cout<<dp[m];
	return 0;
} 

三、多重度背包

问题描述:有 N 种物品和一个容量是 V 的背包。第 i 种物品最多有 s[i] 件,每件体积是 w[i],价值是 c[i]。求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。

朴素算法

:多重度背包就是物品可以取规定次数,我们直接可以加一次循环for(int k=0;k<=s[i]&&v[i]*k<=j;k++);来实现取规定次数

代码示例:

#include<iostream>
#include<algorithm>
using namespace std;
int n,m,w[10005],c[10005],dp[10005],s[10005];
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>w[i]>>c[i]>>s[i];
	}
	for(int i=1;i<=n;i++){
		for(int j=m;j>=w[i];j--){//j<w[i]没有选择的机会
			for(int k=0;k<=s[i]&&w[i]*k<=j;k++){//保证取k此次物品重量小于背包容量
				dp[j]=max(dp[j],dp[j-w[i]*k]+c[i]*k);//价值重量都需要乘k
			} 
			
		}
	}
	cout<<dp[m];
	return 0;
} 

拆包装包优化(拆成01背包问题)

可以将此问题转化为一个01背包问题,将当前物品的s[i]件都装入背包,然和当成01背包问题处理即可
装包代码:

int k=N+1;
	for(int i=1;i<=N;i++){
		while(s[i]>1){//说明有多个物品
			 v[k]=v[i];
			 w[k]=w[i];
			 k++;
			 s[i]--;
		}
	} 

完整代码示例:

#include<iostream>
#include<algorithm>
using namespace std;
const int M=500005;
int N,V,v[M],w[M],s[M],dp[M];
int main(){
	cin>>N>>V;
	for(int i=1;i<=N;i++){
		cin>>v[i]>>w[i]>>s[i];
	} 
	//装包(转化为01背包问题)
	int k=N+1;
	for(int i=1;i<=N;i++){
		while(s[i]>1){//说明有多个物品
			 v[k]=v[i];
			 w[k]=w[i];
			 k++;
			 s[i]--;
		}
	} 
	for(int i=1;i<=k;i++){
		for(int j=V;j>=v[i];j--){
			dp[j]=max(dp[j],dp[j-v[i]]+w[i]); 
		}
	}
	cout<<dp[V]<<endl;
	return 0;

上述两种方法时间复杂度都过高,一般采用如下方法解决多重度背包问题

二进制优化方法(优化时间复杂度)

二进制优化方法,就是将物品拆成2的n次方个放入背包中,这样可以组成任意个数(小小的数学规律):
例如:当前物品有7个,拆成1,2,4,3个放入背包中(注意2的n次方不得大于背包容量,如上2^2还剩3个,则3个单独放入背包中),然后当成01背包处理,会发现其实拿1个,2个,3个,4个,5个,6个,7个都可以由这些个数的选择与不选择数得出,这就大大减少了时间复杂度
代码示例:

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int n,m,dp[500005];
struct Good{
	int w,c;
};
int main(){
	cin>>n>>m;
	vector<Good>goods;
	//拆包,装包
	for(int i=1;i<=n;i++){
		int w,c,s;
		cin>>w>>c>>s;
		for(int k=1;k<=s;k*=2){
			s-=k;
			goods.push_back({w*k,c*k});
		}
		if(s>0){//剩余件数放入背包中
			goods.push_back({s*w,s*c}); 
		}  
	}
	/*for(auto good: goods){
		for(int j=m;j>=good.v;j++){
			dp[j]=max(dp[j],dp[j-good.v]+good.w);
		}
	}*/
	//接下来直接当成01背包处理
	for(int i=0;i<=goods.size();i++){//这里注意,vector数组的下标是从0开始的,背包中物品的种类不再是n个,而是vector数组的大小 
		for(int j=m;j>=goods[i].w;j--){
			dp[j]=max(dp[j],dp[j-goods[i].w]+goods[i].c);
		}
	} 
	cout<<dp[m]<<endl;
	return 0;
}
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值