动态规划之0-1背包问题&&完全背包问题

一.01背包

1.问题描述

对于01背包问题题干,移步至AcWing  👇👇👇 

2. 01背包问题 - AcWing题库icon-default.png?t=N7T8https://www.acwing.com/problem/content/2/ 
 

代码按照下题干编写:

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i 件物品的体积是 c [ i ],质量是 w [ i ]

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总质量最大
输出最大质量

 2.问题分析

对于动态规划问题,我们主要从两个方面分析:

状态表示状态计算

对于状态表示,要考虑怎么用dp数组,用几维的dp数组,用dp数组来表示什么状态

一般来说 题目的限制条件个数 ,决定了dp数组的维度

例如01背包问题中的限制条件有体积c [ i ]  (对应背包容积)和物品总个数

 按照y总的闫式DP分析法  图解如下

3.二维dp代码 

根据图解 可以写出代码 👇👇👇

//二维01背包
//状态转移方程 :dp[i][j]=max(dp[i-1][j-volume[i]]+weight[i] , dp[i-1][j])
//前i个物品 容量为j      选取当前物品(物品-1,背包容量-当前物品体积)
//                       不选取当前物品 (最大值必定是前i-1个物品,容量为j的最大值)
//选与不选取最大值

//二维01背包(代码演示)
#include<bits/stdc++.h>
using namespace std;
int dp[1001][1001],w[1000],c[1000];
int main()
{
	int n,V,i,j;
		//n件物品 背包容量V
		cin>>n>>V;
		for(i=0;i<n;i++) cin>>w[i];
		for(i=0;i<n;i++) cin>>c[i];
		memset(dp,0,sizeof(dp));
		for(i=0;i<n;i++){
			for(j=c[i];j<=V;j++){
				//状态转移方程
				//capacity weight
			dp[i][j]=max(dp[i-1][j],dp[i-1][j-c[i]]+w[i]);
			}
		cout<<dp[n][V];
	}
}

4.代码优化过程

👆👆👆

 通过观察上述代码,我们可以发现,那么对于我们的程序,时间上 01背包问题的时间复杂度为O(N*W),其中N为物品的数量,W为背包的最大容量,时间复杂度无需优化

但是当求 dp[i][j]时,每次用的是dp[i-1][…],也就是用的第i-1层所求出来的结果,所以从空间上,我们可以对我们的程序进行优化,用一维的dp数组就可以解决该问题


 当把我们的状态转移方程

 dp[i][j]=max(dp[i-1][j],dp[i-1][j-c[i]]+w[i]);里含i的那一维删除后 成了

dp[j]=max(dp[j],dp[j-c[i]]+w[i]);

所以压缩后的状态转移方程的括号中的dp[ j ]代表的是dp[ i-1 ][ j ]也就是上一层的dp[ j ]的结果(这个的问题不大,因为现在要求第i层的dp[ j ],那么dp[ j ]里面存放的自然就是上一层所求出的dp[ j ]的结果,只有当max函数比较完毕后,dp[ j ]才从第i-1层的结果更新成了第i层,也就是当前层的结果

关于 dp[ j - c[ i ] ] 代表的也是上一层的结果,也就是dp[ i - 1 ][ j - c[ i ] ]

(那么这个就要考虑 j 在循环中是递增还是递减的了,如果要保证dp[ j - c [ i ] ]代表的是dp[ i - 1 ][ j - c [ i ] ]就应该让dp[ j ]先于dp[ j - c[ i ] ]求出来

这样的话,求dp [ j ]时,在状态转移方程dp[j]=max(dp[j],dp[j-c[i]]+w[i])

括号中的dp [ j ]还没求出,因为正在求,dp [ j ] 还保留着上一层的结果,也就是dp [ i - 1 ][ j ]

并且由于dp[ j - c [ i ] ]还没求出,它代表的也是上一层的值,也就是dp[ i - 1 ] [ j - c[ i ] ]

此时刚好和我们的二维数组的dp相吻合,所以如果想进行空间优化,只要让dp[ j ] 比 dp[ j - c[ i ] ]先求出来就好,也就是让循环中的 j 由从小到大改为从大到小循环就好,此时因为 j>j-c[i],所以大的先求出,也就是dp [ j ] 先求出,dp [ j - c[ i ] ]后求出,与我们的预期相同)


5.一维dp代码

上述文字表述的代码表示如下

(为方便读者观看,代码中仍有注释用来解释,与上面的文字表述意思相同)👇👇👇

//二维01背包
//状态转移方程 :dp[i][j]=max(dp[i-1][j-volume[i]]+weight[i] , dp[i-1][j])
//前i个物品 容量为j      选取当前物品(物品-1,背包容量-当前物品体积)
//                       不选取当前物品 (最大值必定是前i-1个物品,容量为j的最大值)
//选与不选取最大值

//优化的心路历程:
//看二维背包发现.对于dp[i][j]都需要用dp[i-1][…]这一层,也就是说都要用dp[i][…]前面那一层
//所以这一层可以省略,只要i从小到大,dp[i]在操纵前存放的永远都是dp[i-1]的值
//然后对于 j 选择从大到小也避免了重复的计算
//因为dp[i][j]的意义本身就是前i个物品 容量为j 
//所以如果是从小到大计算的话  	dp[j]=max(dp[j],dp[j-c[i]]+w[i]); 中的dp[j-c[i]]会有重复
//(从小到大的时候,j-c[i]<j,所以dp[j-c[i]]一定是比dp[j]先算出来的,这样就会有重复)
//但是从大到小的话dp[j]和dp[j-c[i]]比较时候一定是没有重复的


//一维01背包(代码演示)
#include<bits/stdc++.h>
using namespace std;
int dp[1001],w[1000],c[1000];
int main()
{
	int n,V,i,j;

		//n件物品 背包容量V
		cin>>n>>V;
		for(i=0;i<n;i++) cin>>w[i];
		for(i=0;i<n;i++) cin>>c[i];
		memset(dp,0,sizeof(dp));
		for(i=0;i<n;i++){
			//倒着循环 防止背包重复装取
			for(j=V;j>=c[i];j--){
				//状态转移方程
				//capacity weight
				dp[j]=max(dp[j],dp[j-c[i]]+w[i]);
			//  dp[i][j]=max(dp[i-1][j],dp[?][j-c[i]]+w[i])
			//  如果是二维的时候,那么应该?处是i-1,那么如何保证呢
			//  假如j是从小到大的,那么由于dp[j-c[i]]一定比dp[j]先算出来
			//  并且就是在这一层算出来的(即i这一层)
			//  所以这里的dp[j-c[i]]+w[i]实际上是与dp[i][j-c[i]]+w[i]等价的
			//  如果让这里的dp[j-c[i]]+w[i]和dp[i-1][j-c[i]]+w[i]等价
			//  就应该使得dp[j-c[i]]比dp[j]后算出来(此时dp[j-c[i]]中仍然是上一层i-1时的数据)
			//  也就是dp[j-c[i]]与dp[i-1][j-c[i]]等效
			//  所以使j从大到小循环,即可保证在第i层时,先算dp[j]再算dp[j-c[i]]
			//  那么dp[j-c[i]]中保存的时第 i 层的数据
			//  此时再做max操作即与dp[i][j]=max(dp[i-1][j],dp[i-1][j-c[i]]+w[i])等价
				
			}

		cout<<dp[V]<<endl;
		
	}
}

二.完全背包

1.问题描述

请移步至AcWing  👇👇👇

3. 完全背包问题 - AcWing题库icon-default.png?t=N7T8https://www.acwing.com/problem/content/3/

代码按照以下题干编写

有 N 件物品和一个容量是 V 的背包。每件物品可使用任意次。

第 i 件物品的体积是 c [ i ],质量是 w [ i ]

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总质量最大
输出最大质量

2.问题分析 

先说结论:

 0-1背包  dp[i][j]=max(dp[i-1][j],dp[i-1][j-v]+w)
完全背包 dp[i][j]=max(dp[i-1][j],dp[i]   [j-v]+w)⭐

可知0-1背包和完全背包的状态转移方程大致相同,但是有个小小的不同,其实分析起来差距很大 

正如 01 背包一样我们仍要从 状态表示 和 状态计算 两方面进行分析,完全背包也如此

完全背包的限制条件也是物品个数 和 背包容量 所以也要 先由二维dp数组表示

对于完全背包问题每个物品可以取任意个
那么对于第 i 个物品,就有取0,1,2,3,4…n 个的情况
(直到总体积大于等于j)(第i 个物品体积为 v 质量为w)
那么也就是
①dp[ i ][ j ]   =max(dp[ i-1 ][ j ],dp[ i-1 ][ j-v ]+w , dp[ i-1 ][ j-2v ]+2w,…,dp[ i-1 ][ j-n*v ]+n*w)
对应第 i 个物品取                   0                    1                            2           …                   n    个的情况
如果背包容积不是j 而是j-v,那么相当于
②dp[ i ][ j-v ]=max(dp[ i-1 ][ j-v ],dp[ i-1 ][ j-2v ]+w,…,dp[ i-1 ][ j-n*v ]+(n-1)*w


①②对比会发现①②大部分相同,①除去首项后相当于整体比②多w
也就是dp[ i ][ j ]=max(dp[ i-1 ][ j] ,②+w)
dp[ i ][ j ] =max(dp[ i-1 ][ j ],dp[ i ][ j-v ]+w)
得出最后的状态转移方程,与0-1背包的像,但是得出结论的历程不一样
对比下
0-1背包   dp[ i ][ j ]=max(dp[ i-1 ][ j ],dp[ i-1 ][ j-v ]+w)
完全背包 dp[ i ][ j ]=max(dp[ i-1 ][ j ],dp[ i ][ j-v ]+w)⭐

用 y总的 闫式dp分析法 的分析图解如下

3.二维dp代码

根据图解 可以写出代码 👇👇👇

//二维完全背包(代码演示)
#include<bits/stdc++.h>
using namespace std;
int dp[1001][1001],w[1000],c[1000];
int main()
{
	int n,V,i,j;
		//n件物品 背包容量V
		cin>>n>>V;
		for(i=0;i<n;i++) cin>>w[i];
		for(i=0;i<n;i++) cin>>c[i];
		memset(dp,0,sizeof(dp));
		for(i=0;i<n;i++){
			for(j=c[i];j<=V;j++){
				//状态转移方程
				//capacity weight
			dp[i][j]=max(dp[i-1][j],dp[i][j-c[i]]+w[i]);
			}
		cout<<dp[n][V];
	}
}

4.代码优化过程

与01背包类似 完全背包也可以用一维数组进行空间优化,降低空间复杂度

 0-1背包  dp[i][j]=max(dp[i-1][j],dp[i-1][j-v]+w)
完全背包 dp[i][j]=max(dp[i-1][j],dp[i]   [j-v]+w)⭐

通过比较可以得知,若想使用一维dp关于 dp[i-1][j] 与01背包的一致即可

但是对于dp[i][j-v]可知与01背包的不同 需要优化

回顾下上面所说的01背包的优化过程:

为了保证 dp[j-v]代表的是dp[i-1][j-v]只要使得dp[j-v]后于dp[j] 也就是 j 从大到小循环即可,此时在

max(dp[j],dp[j-v])中,dp[j]代表dp[i-1][j]      dp[j-v]代表dp[i-1][j-v]

所以 此时我们想让 dp[j-v]代表dp[i][j-v]  只需要再让 j 从小到大循环即可

5.一维dp代码

完全背包一维dp代码如下 👇👇👇

//一维完全背包(代码演示)

#include<bits/stdc++.h>
using namespace std;
int dp[1001],w[1000],c[1000];
int main()
{
	int n,V,i,j;

		//n件物品 背包容量V
		cin>>n>>V;
		for(i=0;i<n;i++) cin>>w[i];
		for(i=0;i<n;i++) cin>>c[i];
		memset(dp,0,sizeof(dp));
		for(i=0;i<n;i++){
			for(j=c[i];j<=V;j++){
				//状态转移方程
				//capacity weight
				dp[j]=max(dp[j],dp[j-c[i]]+w[i]);
                //j 正着循环 由于 j > j-c[i]
                //所以在第i层求dp[j]时,dp[j-c[i]]已经在此层(第i层)被求出
                //也就是说dp[j-c[i]]在此时实际是dp[i][j-c[i]]
                //dp[j]在此时是dp[i-1][j]
                //符合二维完全背包的逻辑
	
			}

		cout<<dp[V]<<endl;
		
	}
}

三.总结

经过优化空间复杂度的过程,我们发现 01背包和完全背包问题优化的主要点是搞清楚

一维的dp数组对应的值是在哪一层所求出来的,然后再对应其二维dp数组的状态转移方程即可

所以在做题时 先根据闫式dp分析法按照 状态表示 和 状态计算 分析

根据题目中的限制写出状态转移方程 再根据状态转移方程判断是否在第 i 层用的都是 第 i 层或第i-1层的数据,进而根据实际情况选择正确的循环方向,进而优化空间复杂度

  • 22
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值