一个简易代码模板,解决三大经典背包问题(01/完全/多重)

一个简易代码模板,解决三大经典背包问题(01/完全/多重)

​​  前言:本篇文章的重点在于自创代码模板的建立与应用,在背包原理的部分可能讲的会偏少偏跳跃,大家可以重点吸收“代码模板部分”的知识,以及代码模板在后半文实战中的应用方法

​​  对于背包原理中的知识若有不清楚之处,可以去搜集其它博主的优质背包博文~
(本文主要思路为本人原创,如有错误,欢迎友友指正~)

相关知识

背包的基本模型❔

​​​  我们有一个容量为V的背包和若干种物品,在一定的限制条件下(每种物品都占用一定容量),问最多能放进多少价值的物品?

背包问题的分类🎒

​​​  01背包完全背包多重背包、二维费用背包、混合三种背包、分组背包和有依赖背包。
​ (PS:最常见也是最基础的背包问题类型就是前三种背包)

01背包

题目问法🌌

​​  有N件物品和一个容量为V的背包。第i件物品的价值为w[i],体积为v[i]。
​​​  求解:背包的可装纳的最大价值是多少?

问题特点

​​​  每种物品仅有一件,可以选择放或不放。

问题解法

​​​  假设有N件物品,背包容量为V,第i件物品的价值为w[i],体积为v[i]。
只考虑前1个物品:
​​​  遍历体积V-v[1],用dp[j]表示背包体积为j时的背包可装纳的最大价值,dp数组使用前均初始化为0。第一次遍历时,显然最大价值就是该物品价值。
只考虑前2个物品:
​​​  遍历体积V-v[2],更新dp数组,但怎么更新dp数组?
​​​  我们可以将dp数组更新时的物品放置情况分为两种:1️⃣dp[j]并不包含第2个物品 2️⃣dp[j]包含第2个物品。

  • 1️⃣: 不包含第2个物品的最大值刚才已经算过,就是老的dp[j],不必更新。

  • 2️⃣: 包含第2个物品的最大值则是第2个物品的价值w[2]➕当背包容量少掉v[2]时的最大价值dp[j-v[2]]。

    综上,我们可以写出此时(只考虑前2个物品)的dp数组更新公式,也就是我们动态规划的“状态转移方程”: dp[j]=max(dp[j],w[2]+dp[j-v[2]);
    ​​​  继续只考虑前3、前4个物品,直到考虑完N个物品,最终的结果就被存储在了dp[N]之中。

完全背包

题目问法🌌

​​​  有N种物品和一个容量为V的背包,每种物品可以无限次重复取。第i件物品的价值为w[i],体积为v[i]。
​​​  求解:背包的可装纳的最大价值是多少?

问题特点

​​​  每种物品有无数件,可以任意选择放不放,放多少个。

问题解法

​​​  与刚才01背包解法完全相同,不过将体积的逆序遍历顺序V-v[i]改为正序的体积遍历顺序v[i]-V

必须装满/求最小值怎么办?

小修改①:如果必须要求背包被装满呢?
​​​  之前我们的题境中,并没有限制必须要让背包装满,如果我们再外加一个条件,必须要让背包装满,那么我们该如何改程序呢?只需要根据“相对原则”修改dp数组的初始化即可。
什么是相对原则?
​​​  如果我们要求最大值,那么我们就把dp数组除dp[0]为0以外所有的数组元素均初始化为负无穷
​​​  如果我们要求最小值,那么我们就把dp数组除dp[0]为0以外所有的数组元素均初始化为正无穷
小修改②:如果要求最小价值呢?
​​  直接修改状态转移方程中的max为min即可。

01&完全🎒总结:三个根据

​  根据背包的名字差异(01背包还是完全背包),决定背包体积层遍历究竟是顺序还是逆序。
​​​  根据背包的限制差异(不装满还是装满),决定dp数组的初始化究竟是直接为0还是根据相对原则。
​​​  根据背包的目的差异(要求最大价值还是最小价值),决定状态转移方程中的最值函数为min还是max。

核心代码模板——01&完全🎒
//选择符合题意的三个代码选择
//程序变量说明:n-物品总数,V-背包体积 v-各物品体积 w-各物品价值 dp[j]-背包体积为j时的最大背包价值
//模板使用指引:
//①:先确定要不要求装满,做出第一个代码选择(选中一个符合的,删除/注释其它的);
//②:再根据是01背包还是完全背包(可不可以无限拿同一件物品),做出第二个代码选择;
//③:最后看是求背包的最大价值还是最小价值,做出第三个代码选择。

//第一个代码选择:
		memset(dp,0,sizeof(dp)); //不要求装满
//		dp[0]=0; //备选1:要求装满,并求背包价值的最大值 
//		for(int i=1;i<=V;i++)dp[i]=-1e9; 
//		dp[0]=0; //备选2:要求装满,并求背包价值的最小值 
//		for(int i=1;i<=V;i++)dp[i]=1e9;

		for(int i=1;i<=n;i++)
//第二个代码选择:
			for(int j=V;j>=v[i];j--) //01背包/多重背包 遍历顺序
//			for(int j=v[i];j<=V;j++) //备选:完全背包遍历顺序 

//第三个代码选择:
				dp[j]=max(dp[j],dp[j-v[i]]+w[i]); //求最大价值
//				dp[j]=min(dp[j],dp[j-v[i]]+w[i]); //备选:求最小价值 
		cout<<dp[V]<<endl;   

多重背包

题目问法🌌

​​  一种物品的个数可能不止1个,因此不是我们熟悉的01背包,同时它的个数也不是无数个,因此也不是我们熟悉的完全背包,它有固定个数相同的物品

问题特点

​​  每种物品有固定件数,可以任意选择放不放,放多少个,但是又最大个数限制。

问题解法

​​​  可能有的同学会想,重复多少次,就改成重复输入多少次1个背包即可,但是这样的问题是,这样的话时间复杂度是O(重复数)级别的,如果重复数非常多,那么很可能会导致TLE。
​​​  那该怎么办,很简单,我们只需要对背包进行二进制分组,然后等效看待成多个不一样的背包即可。
​​​  例如,我们有10个体积为2,价值为4的物品。我们可以将它拆分4组:第一组1个,第二组2个,第三组3个,第四组4个。然后我们把每一组等效为一个新物品。如果按以上的方式分组,那么等效的结果就是:
​​  ​​  第一组→1个体积为2,价值为4的物品;
​​  ​​  第二组→1个体积为4,价值为8的物品;
​​  ​​  第三组→1个体积为6,价值为12的物品;
​​  ​​  第四组→1个体积为8,价值为15的物品。
​​​  这样一来,10个体积为2,价值为4的重复物品,就变成了以上4种具有不同价值与体积的新物品,我们就可以使用01背包来进行熟悉的问题求解了,而此时的时间复杂度为O(log2重复数),时间复杂度剧减。
​​​  大体思路是这样的,但是具体该如何细分呢?换句话说,细分的规则是什么?
​​​  答案是,根据二进制原理,2的幂可以表示一切正整数的特性,我们利用2的幂来分组。
​​​  2的幂是1、2、4、8、16、…、2n。我们的分配思路是,按照二进制数从小到大进行分配,分配到不能分为止,剩下的就单独成为一组。
​​​  还是以刚才的10个重复物品为例,我们的分配流程如下所示:

分配前剩余数分配数分配后剩余数数
第一次分配1019
第二次分配927
第三次分配743
第四次分配333

​​​  由上表清晰可见,前三次分配都是分配一个二次幂第四次分配本来要分配8,但是显然分配前剩余数不够再分出那么多了,那么我们就按原则,剩下不能分的,单独成为一组即可,剩下是3,就按3为分配数进行最终分配。

核心代码模板——多重🎒

​​  如下(以有x个重复的物品,它们的物品价值为a,体积为b为例):

void classify(){ //以下的所有变量均要定义为全局变量
		t=1,cnt=1; //t代表剩余二进制分配数,cnt代表目前是第几次分配
		while (x>=t){
			v[cnt]=a*t,c[cnt++]=b*t;
			x-=t,t<<=1;
		}
		if(x){
			v[cnt]=a*x,c[cnt]=b*x;
		} 
}

​​  下面,我们来测试一下这个核心代码模板的效果,我们随机选取两组数据,测试结果如下(结果令人满意):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-77EudcHA-1646738317440)(C:\Users\49593\AppData\Roaming\Typora\typora-user-images\image-20220307192018878.png)]
​​  测试代码如下:

#include<bits/stdc++.h>
using namespace std;
int x,a,b,v[500],c[500],t,cnt;
void classify(){
		t=1,cnt=1; 
		while (x>=t){
			v[cnt]=a*t;
			c[cnt++]=b*t;
			x-=t;
			t<<=1;
		}
		if(x){
			v[cnt]=a*x;
			c[cnt]=b*x;
		} 
}
int main(){
	ios::sync_with_stdio(false);
	while(cin>>x>>a>>b){
		cout<<"分类前: "<<endl<<x<<"个价值为"<<a<<" 体积为"<<b<<"的物品"<<endl; 
		memset(v,0,sizeof(v));
		memset(c,0,sizeof(c));
		classify();
		cout<<"分类后: " <<endl;
		for(int i=1;i<=cnt;i++)cout<<"1个价值为"<<v[i]<<" 体积为"<<c[i]<<"的物品"<<endl;
	}	
	return 0;
} 

如何应用以上的分配代码?

​​  多重背包问题,一般在输入时,都是输入“该类型物品数量 该类型物品体积 该类型物品价值”,如下所示:

for(int i=1;i<=n;i++) //假设有n种背包
cin>>bag_num>>vv>>ww; //依次输入第i种物品的数量,体积和价值

​​  我们引入cnt来统计“等效分配”(刚才所说的多重背包的内容)后的等效物品数量(而不是n),一开始,我们令cnt的值为1,最后令01&完全背包中的物品数n=cnt-1,方便后续01背包模板的套用:

	int cnt=1;
	for(int i=1;i<=n;i++){ 
		cin>>item_num>>vv>>ww;
			int t=1; 
			while(item_num>=t){
				v[cnt]=vv*t,w[cnt++]=ww*t;
				item_num-=t,t<<=1;
			}
			if(item_num){
				v[cnt]=vv*item_num,w[cnt++]=ww*item_num;
			}
	}
	n=cnt-1; //最后将分配前的物品总数改为分配后的物品总数,方便后续01背包模板的套用 

💥王炸: 三背包の万能代码模板

​​  最终,我们将三种背包混搭在一起,就有了基础三背包の万能代码模板,以下代码注释非常详细,如果还是不能理解可以看作者本人是如何用此模板秒杀背包题的。

//--------------------------01背包&完全背包&多重背包の万能模板--------------------------//

//多重背包分配代码,如果是多重背包,则取消下列代码的注释,根据不同题目,输入格式可能要稍微改动 
//程序变量说明:n-分配前物品数 最后的cnt-分配后物品数 item_num-当前物品的重复数量 vv-当前物品的体积 ww-当前物品的价值 t-当前二进制分配数 v[cnt]第cnt个物品的体积 w[cnt]第cnt个物品的价值  
//	int cnt=1;
//	for(int i=1;i<=n;i++){ 
//		cin>>item_num>>vv>>ww;
//			int t=1; 
//			while(item_num>=t){
//				v[cnt]=vv*t,w[cnt++]=ww*t;
//				item_num-=t,t<<=1;
//			}
//			if(item_num){
//				v[cnt]=vv*item_num,w[cnt++]=ww*item_num;
//			}
//	}
//	n=cnt-1; //最后将分配前的物品总数改为分配后的物品总数,方便后续01背包模板的套用 

//选择符合题意的三个代码选择
//程序变量说明:n-物品总数,V-背包体积 v-各物品体积 w-各物品价值 dp[j]-背包体积为j时的最大背包价值
//模板使用指引:
//①:先确定要不要求装满,做出第一个代码选择(选中一个符合的,删除/注释其它的);
//②:再根据是01背包还是完全背包(可不可以无限拿同一件物品),做出第二个代码选择;
//③:最后看是求背包的最大价值还是最小价值,做出第三个代码选择。

//第一个代码选择:
		memset(dp,0,sizeof(dp)); //不要求装满
//		dp[0]=0; //备选1:要求装满,并求背包价值的最大值 
//		for(int i=1;i<=V;i++)dp[i]=-1e9; 
//		dp[0]=0; //备选2:要求装满,并求背包价值的最小值 
//		for(int i=1;i<=V;i++)dp[i]=1e9;

		for(int i=1;i<=n;i++)
//第二个代码选择:
			for(int j=V;j>=v[i];j--) //01背包/多重背包 遍历顺序
//			for(int j=v[i];j<=V;j++) //备选:完全背包遍历顺序 

//第三个代码选择:
				dp[j]=max(dp[j],dp[j-v[i]]+w[i]); //求最大价值
//				dp[j]=min(dp[j],dp[j-v[i]]+w[i]); //备选:求最小价值 
		cout<<dp[V]<<endl;   

相关题目

基础背包问题,本质上就是三个维度选择的随机组合。

​​   第一:是什么类型的背包?是01背包、完全背包,还是多重背包?是哪种背包,就选用哪种背包的模板。

​​  第二:要不要求我们装满背包?根据这个要求装满与否,就选用模板中相应的dp数组的初始化方式。

​​   第三:究竟是求最大值还是最小值?灵活修改最值函数。

​​  将以上三个维度定位准确之后,我们只需要选择相应的代码,做细微的调整,题目就解决了。对于以下5道题,若熟练掌握模板,10分钟一定可以搞定全题,说明这个代码普适性较强,效率也较高。

1、骨头收藏家

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3c4poqyI-1646738317441)(C:\Users\49593\AppData\Roaming\Typora\typora-user-images\image-20220308190410123.png)]

参考思路

​​​  根据题意,可将该题目定位为"01背包+不要求装满+求背包最大价值",代入模板稍作修改即可。

参考AC代码
#include<bits/stdc++.h>
using namespace std;
int main(){
	ios::sync_with_stdio(false);
	int c,n,V,v[1005],w[1005],dp[1005];
	cin>>c;
	while(c--){
		cin>>n>>V;
		memset(v,0,sizeof(v));
		memset(w,0,sizeof(w));
		memset(dp,0,sizeof(dp));//不要求装满
		for(int i=1;i<=n;i++)cin>>w[i];		
		for(int i=1;i<=n;i++)cin>>v[i];
		for(int i=1;i<=n;i++)
			for(int j=V;j>=v[i];j--)//01背包
                dp[j]=max(dp[j],dp[j-v[i]]+w[i]);//求背包最大价值
		cout<<dp[V]<<endl;
	}
	return 0;
}

2、Piggy-Bank

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-INH7Umz6-1646738317442)(C:\Users\49593\AppData\Roaming\Typora\typora-user-images\image-20220308190015640.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sarKfTNb-1646738317442)(C:\Users\49593\AppData\Roaming\Typora\typora-user-images\image-20220308173310390.png)]

参考思路

​​​  根据题意,可将该题目定位为“完全背包+要求装满+求背包最小价值”,代入模板即可。

参考AC代码
#include<bits/stdc++.h>
using namespace std;
const int inf=1e9;//要求装满
#define endl '\n'
int c,a,b,V,n,w[505],v[505],dp[1001000]; 
int main(){
	cin>>c;
	ios::sync_with_stdio(false);
	while(c--){
		cin>>a>>b;
		V=b-a;
		cin>>n;
		memset(w,0,sizeof(w));
		memset(v,0,sizeof(v));
		for(int i=1;i<=n;i++)cin>>w[i]>>v[i];
		dp[0]=0;  //要求装满
		for(int i=1;i<=V;i++)dp[i]=inf; //要求装满
		for(int i=1;i<=n;i++)
			for(int j=v[i];j<=V;j++)//完全背包
				dp[j]=min(dp[j],dp[j-v[i]]+w[i]);//求背包最小价值 
		if(dp[V]==inf){
			cout<<"This is impossible."<<endl;	
		}
		else{
			cout<<"The minimum amount of money in the piggy-bank is "<<dp[V]<<"." <<endl;
		}
	}
	return 0;
}

3、珍惜现在,感恩生活

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0pOFcXTv-1646738317443)(C:\Users\49593\AppData\Roaming\Typora\typora-user-images\image-20220308184930361.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AIl0LxIG-1646738317444)(C:\Users\49593\AppData\Roaming\Typora\typora-user-images\image-20220308173737426.png)]

参考思路

​​​  根据题意,可将该题目定位为“多重背包+不要求装满+求背包最大价值”,代入模板即可。

参考AC代码
#include<bits/stdc++.h>
using namespace std;
const int inf=1e9;
#define endl '\n'
int c,a,b,V,n,w[505],v[505],dp[1001000],item_num,vv,ww; 
int main(){
	cin>>c;
	ios::sync_with_stdio(false);
	while(c--){
		cin>>V>>n;
		memset(w,0,sizeof(w));
		memset(v,0,sizeof(v));
//--------------------------------------多重背包--------------------------------------
        int cnt=1;
		for(int i=1;i<=n;i++){ 
			cin>>vv>>ww>>item_num;
				int t=1; 
				while(item_num>=t){
					v[cnt]=vv*t,w[cnt++]=ww*t;
					item_num-=t,t<<=1;
				}
				if(item_num){
					v[cnt]=vv*item_num,w[cnt++]=ww*item_num;
				}
		}
		n=cnt-1; 
//--------------------------------------多重背包--------------------------------------
		memset(dp,0,sizeof(dp)); //不要求装满
		for(int i=1;i<=n;i++)
			for(int j=V;j>=v[i];j--) //多重背包转化后为01背包,故逆序
				dp[j]=max(dp[j],dp[j-v[i]]+w[i]); //求背包最大价值
		cout<<dp[V]<<endl; 
	}
	return 0;
} 

4、寒冰王座

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X5txC4BM-1646738317444)(C:\Users\49593\AppData\Roaming\Typora\typora-user-images\image-20220308190739573.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jp7J3JYI-1646738317445)(C:\Users\49593\AppData\Roaming\Typora\typora-user-images\image-20220308174127467.png)]

参考思路

​​​  根据题意,可将该题目定位为“完全背包+不要求装满+求背包最大价值”,代入模板即可。
​​​  PS:此题的价值对标价格,体积也对标价格,因此价格既是体积也是价格,这点需要注意,灵活转化一下。

参考AC代码
#include<bits/stdc++.h>
using namespace std;
const int inf=1e9;
#define endl '\n'
int c,a,b,V,n,w[505],v[505],dp[1001000],item_num,vv,ww; 
int main(){
	cin>>c;
	ios::sync_with_stdio(false);
	while(c--){
		cin>>V;
		memset(w,0,sizeof(w));
		memset(v,0,sizeof(v));
		v[1]=150,w[1]=150;
		v[2]=200,w[2]=200;
		v[3]=350,w[3]=350;
		memset(dp,0,sizeof(dp)); //不要求装满
		for(int i=1;i<=3;i++)
			for(int j=v[i];j<=V;j++) //完全背包
				dp[j]=max(dp[j],dp[j-v[i]]+w[i]); //求背包最大价值
		cout<<V-dp[V]<<endl; 
	}
	return 0;
} 

5、减肥记

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nKSP7V5P-1646738317445)(C:\Users\49593\AppData\Roaming\Typora\typora-user-images\image-20220308190929767.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nqtMoNXe-1646738317446)(C:\Users\49593\AppData\Roaming\Typora\typora-user-images\image-20220308174517906.png)]

参考思路

​​​  根据题意,可将该题目定位为“完全背包+不要求装满+求背包最大价值”,代入模板即可。

参考代码
#include<bits/stdc++.h>
using namespace std;
const int inf=1e9;
#define endl '\n'
int c,a,b,V,n,w[505],v[505],dp[1001000],item_num,vv,ww; 
int main(){
	ios::sync_with_stdio(false);
	while(cin>>n){
		memset(w,0,sizeof(w));
		memset(v,0,sizeof(v));
		for(int i=1;i<=n;i++){
			cin>>w[i]>>v[i];
		}
		cin>>V;
		memset(dp,0,sizeof(dp)); //不要求装满
		for(int i=1;i<=n;i++)
			for(int j=v[i];j<=V;j++) //完全背包
				dp[j]=max(dp[j],dp[j-v[i]]+w[i]); //求背包最大价值
		cout<<dp[V]<<endl; 
	}
	return 0;
} 

后言

​​  若阅读中有任何问题,可以在评论区留言~
​​  ​​  祝点赞的各位彭于晏们码力飞进!!!谢谢~

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值