当前位置:   article > 正文

【动态规划】背包问题(01背包,完全背包,多重度背包)_有一个容量为 m 的背包 , 有 n 件物品可以挑选 , 每个物品有 2 个属性

有一个容量为 m 的背包 , 有 n 件物品可以挑选 , 每个物品有 2 个属性

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


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

三种背包

  • 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;
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

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

观察上面代码过程可以发现,我们处理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;
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

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

在上面的代码中,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;
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

二、完全背包

问题描述:有 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;
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

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

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

同样是选择与不选择
不选择: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)
  • 1

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

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

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

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

可见此时选择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;
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

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

二维完全背包同样可以优化成一维数组,减少空间复杂度
优化方法:顺向处理一维数组即可
顺向处理可以实现重复选择
具体结合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;
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

三、多重度背包

问题描述:有 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;
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

拆包装包优化(拆成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]--;
		}
	} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

完整代码示例:

#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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

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

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

二进制优化方法,就是将物品拆成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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/332844
推荐阅读
相关标签
  

闽ICP备14008679号