01背包详解,优化后的一维01背包,枚举顺序?如何求背包选择的物品是哪些?

目录

经典01背包

问题描述:

思路:

模拟:

代码:

思考:

复杂度:

如何求出01背包中具体选择了每些物品?

代码:

空间优化后的一维01背包:

思想:

为什么要倒序枚举?

代码:


经典01背包

问题描述:

有 N 件物品,每件物品有两个属性:价值和重量,第 i 件物品的价值为 v[i] ,重量为 w[i] ,你有一个容量为 M 的背包,求能装下物品的最大价值。

例如有4件物品,背包容量为10。物品价值和重量如下表

样例的答案是:13    (选择第1 2 4件物品,价值最大是13)

思路:

现在用一个二维数组 f[i][j] 来表示前 i 件物品在背包剩余容量为 j 时所能得到的最大价值。(先不要考虑为什么这样定义,后边会再解释)。这里要注意,至于 f[i][j] 在前 i 件物品中选择了哪些,我们是不知道的,我们只知道在只涉及前 i 件物品,并且背包剩余空间是 j 的时候,获得的最大价值是 f[i][j] 。

如果假设我们对前 i-1 件物品已经做好了选择,那么在面对第 i 件物品时,我们就只有两种选择:花费 w[i] 的容量装下他来获得 v[i] 的价值,或者不选择它。选择与不选择,这也是01背包名字的由来。

在已知 f[i-1][j] 的情况下,对于前面所说的选择或者不选择第 i 件物品,我们就可以用式子表示出来:也就是最重要的递推公式:
选择第 i 件物品, f [ i ] [ j ] = f [ i - 1 ] [ j - w [ i ] ] + v [ i ]   ;
不选择第 i 件物品,f [ i ] [ j ] = f [ i - 1 ] [ j ] ;

不选择的情况很好理解,f [ i ] [ j ] = f [ i - 1 ] [ j ] ;背包剩余容量不变,价值也不变,只是原来是只考虑前 i-1 件,现在考虑到前 i 件了。
选择的话,新的价值计算应该是 f [ i - 1 ] [ j - w [ i ] ] + v [ i ],也就是要用前 i 件物品在背包余量为 f [ i - 1 ] [ j - w [ i ] ] 的最大价值,而不是f [ i - 1 ] [ j ],因为要提前为第 i 件物品的重量 w[i] 提供空间,才能代表选择了第 i  件物品,才能使其最大价值加上 v[i] 。

这里也很明显看出,j > w[i] 装得下才可以选择,如果背包余量不够的话,直接就是不选择。

选不选的情况我们讨论了,最终决策要看两种情况哪个价值更大,所以要从中取max,最核心的代码如下:

    if (j>=w[i])  f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i]);		
        else f[i][j]=f[i-1][j];	

模拟:

我们按照样例模拟一下 f[i][j] 的求解过程,如果你在学习之后能独立填出这个 f 数组,你也就真正意义上理解了01背包。
注意:i 代表第 i 件物品, j 代表背包剩余容量。

初始表格如下:

在0件物品的时候,不管背包多大,最大价值都是0,同理,在背包容量是0的时候,不管有几件物品,最大价值也还是0。所以可以很容易把表填成如下图所示:(对0这种情况的考虑,主要是针对边界,因为后面的计算会用到f[0][5],f[2][0]之类的带有下标0的量。)

接下来,我们第一件物品,第两件物品的从上到下,背包容量为0 1 2 3的从左到右来填写表格。
当i=1时,即只有物品1可供选择,在容量不足时,装不下,价值为0,那么如果容量足够的话,最大价值自然就是物品1的价值了。

当i=2时,有两个物品可供选择,此时应用上面的递推关系式进行判断即可。这里以i=2,j=3为例进行分析。
在容量为0-2时,第2件物品装不下,自然无法选择,所以f[i][j]=f[i-1][j];
在容量为3时,可以选择第2件物品了,至于到底选不选,我们需要比较一下:
不选的话 f[2][3]=f[1][3]=2; 选择的话,f[2][3]=f[1][3-w[2]]+v[2]=f[1][0]+4=4;
因为选择价值更大,所以我们用更大的4来填充f[2][3]。

后面的表格,我们一样按照这个规矩填写,只要当前背包剩余容量 j 大于当前第 i 件物品的重量,也就是说能装下的情况,我们都会比较一下试试,看看哪个结果更优。

最终得到表格:

最终的答案也就是f[n][m]。

代码:

#include <iostream>
using namespace std;

int f[1005][1005]; 
//f[i][j]表示前i件物品剩余空间j的情况下最大价值是f[i][j] 
int v[1005];
int w[1005];
int n,m;

int main(){
	cin>>n>>m;
	for (int i=1; i<=n; i++){
		cin>>v[i]>>w[i];
	}
	for (int i=1; i<=n; i++){
		for (int j=1; j<=m; j++){
			if (j>=w[i]) f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i]);		
		        else f[i][j]=f[i-1][j];	
		}
	}
	for (int i=0; i<=n; i++){
		for (int j=0; j<=m; j++) cout<<f[i][j]<<" ";
		cout<<endl; 
	}
	cout<<f[n][m]<<endl;
}

思考:

1.为什么模拟填表格是按照从上到下,从左到右的顺序?枚举变量i,j是从小到大的顺序?
  对于物品的枚举变量 i ,从递推公式中可以看出,第 i 件物品的决策要用到 i-1 的答案,也就是说只有先把第 i-1 件物品处理完,得到了f[i-1][j],才能继续处理第 i 件物品的决策。所以 i 对于物品的枚举,必须是从小到大。
  对于容量的枚举变量 j ,f[i][j] 用到的是 f[i-1][j] 和 f[i-1][j-w[i]] , 也就是说同一个物品的容量 j 枚举是没有顺序的,因为用到的都是f[i-1]那一层的数据,是已经存在的,j 的枚举从大到小还是从小到大是没有区别的。

2.f[i][j]数组为什么这么定义
  我们定义的 f[i][j] 来表示前 i 件物品在背包剩余容量为 j 时所能得到的最大价值,这是因为这样定义最容易理解,或者最容易状态转移,你也可以随心定义 f[i][j] 表示前 i 件物品花费了 j 的容量所得到的最大价值,那么你就要想出对应的递推公式,总之,定义状态是为了更好的求解,所以动态规划问题想轻松解决的话,需要定义一个好的状态,如果你的状态定义的很差,那么你将花更多时间去推导递推公式。

复杂度:

时间复杂度是O(n*m),空间复杂度为O(n*m),空间复杂度可优化为O(m),后边会讲空间优化。

 

如何求出01背包中具体选择了每些物品?

利用倒推的思想,很容易在二维数组f[i][j]中求出具体选择了哪些物品。

首先,二维数组每个格子的数据来源就只有两个,要么是没有选择第 i 件物品,那么 f [ i ] [ j ] = f [ i - 1 ] [ j ] ;要么是选择了第 i 件物品,那么f [ i ] [ j ] = f [ i - 1 ] [ j - w [ i ] ] + v [ i ]   ;所以我们只需要倒着从 f[n][m] 开始,查看以上两个等式哪个成立,如果f [ i ] [ j ] = f [ i - 1 ] [ j ]x ,那就说明第 i 件物品没被选,j 也不需要改动,反之就是被选择了,对应的背包容量 j 要减去 w[i] 。

代码:

#include <iostream>
using namespace std;

int f[1005][1005]; 
//f[i][j]表示前i件物品剩余空间j的情况下最大价值是f[i][j] 
int v[1005];
int w[1005];
int n,m;

int main(){
	cin>>n>>m;
	for (int i=1; i<=n; i++){
		cin>>v[i]>>w[i];
	}
	for (int i=1; i<=n; i++){
		for (int j=m; j>=1; j--){
			if (j>=w[i]) f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i]);		
				else f[i][j]=f[i-1][j];		
		}
	}
	int j=m;
	for (int i=n; i>=1; i--){
		if (f[i][j]!=f[i-1][j]) {
			j=j-w[i];
			cout<<i<<" "; 
		}
	}
	cout<<endl<<f[n][m]<<endl;
}

 

空间优化后的一维01背包:

思想:

在上面的探讨中我们发现,第 i 件物品的决策只与第 i-1 件有关,与其他无关, f[i][j] 只与 f[i-1][j] 有关,i-2,i-3 这些空间的数据是不会再使用的,空间就浪费了,如果我们开一维的数组,新的状态直接覆盖在旧的上面,迭代使用,就把空间复杂度从O(n*m)优化为O(m)。

为什么要倒序枚举?

直接将二维数组改为一维是不够的,对于枚举顺序,我们也要有约定,在思考的问题1中我们知道,二维01背包的枚举变量 j 顺序是无所谓的,因为用到的是 i-1 的已存在数据,如果优化为一维01背包的话,枚举顺序就必须是倒序。
如果还是正序的话,f[i][j]在使用f[i-1][j]的时候,那个旧的f[i-1][j]已经被覆盖了,你使用的是新的f[i][j]。

例如计算f[3][8]即当枚举到 i=3,j=8时,(也就是说你在枚举到第3个物品【重量为5,价值为3】,当前背包容量j为8),计算时需要使用f[i-1][j-w[i]],也就是f[2][3],如果 j 的枚举是正序,意味着你在求 f[3][8] 之前就会先计算 f[3][3],而因为优化空间了,f[3][3]存放的位置是f[2][3],所以这个时候你使用的就不再是f[2][3],而是已经覆盖的f[3][3]。这样明显会让答案不正确并且偏大。

倒序枚举 j 就解决了这种覆盖的问题,因为大的j只会用到前面的小的j,在倒序中,不会覆盖i-1的数据。

代码:

#include <iostream>
using namespace std;

int f[1005]; 
//f[j]表示前i件物品剩余空间j的情况下最大价值是f[j] 
int v[1005];
int w[1005];
int n,m;

int main(){
	cin>>n>>m;
	for (int i=1; i<=n; i++){
		cin>>v[i]>>w[i];
	}
	for (int i=1; i<=n; i++){
		for (int j=m; j>=1; j--){
			if (j>=w[i]) f[j]=max(f[j],f[j-w[i]]+v[i]);		
		}
	}

	//for (int j=0; j<=m; j++) cout<<f[j]<<" "; cout<<endl; 

	cout<<f[m]<<endl;
}

再改进下:j >= w[i] 才执行,所以可以直接枚举到w[i],不再需要 if 判断。

#include <iostream>
using namespace std;

int f[1005]; 
//f[j]表示前i件物品剩余空间j的情况下最大价值是f[j] 
int v[1005];
int w[1005];
int n,m;

int main(){
	cin>>n>>m;
	for (int i=1; i<=n; i++){
		cin>>v[i]>>w[i];
	}
	for (int i=1; i<=n; i++){
		for (int j=m; j>=w[i]; j--){
			f[j]=max(f[j],f[j-w[i]]+v[i]);		
		}
	}

	//for (int j=0; j<=m; j++) cout<<f[j]<<" "; cout<<endl; 

	cout<<f[m]<<endl;
}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值