完全背包问题+01背包问题+分组背包+多重背包 总结

背包问题都涉及到动态规划,利用dp进行更加优化的计算。

一、01背包

最基本的是01背包问题,题目一般类似:“在一定数目物品内,挑选总重量不超过一定数目的物品,其中每个物品只能选一次,求背包内物品价值的最大值或者最小值”,从名字就可以看出,要么选0个,要么选1个。

如果按照暴力的方法,时间复杂度会爆表,这里采用的是记忆化搜索的方式,加以DP。

首先,选一个二维数组dp,这个数组的含义是从前i个物品中选出总重量不超过j的物品时总价值的最大值,也就是说,对于数组中的任意一个元素dp[i][j],其含义为在前i个物品中选出总重量不超过j的物品的最大值。

定义好了数组,接下来就是找状态转移方程。最容易找到的就是最初的状态,即dp[0][j]=0,在一个物品也不选的时候,不论总质量是多少,总价值肯定是0。接下来的状态都是在这个基础状态上进行扩展。既然是放物品,那么肯定是物品一个一个遍历,如果能放不下,其最大价值肯定还等于上一个状态,也就是说当j=w[i]时,dp[i+1][j]=max(dp[i][j],dp[i][j-w[i]]+v[i]),这样所有的状态我们就找到了,即状态转移方程为:
dp[0][j]=0;
dp[i+1][j]=dp[i][j] i dp[i+1][j]=max(dp[i][j],dp[i][j-w[i]]+v[i]) j>=w[i]

根据这个就可以写出01背包最大值问题的模板了

for(int i=0;i

在这个的基础上可以进行空间复杂度上的优化,不难发现每次对下一行的数据的更新都是取决于上一行的数据,所以并不一定需要开一个二维数组,一维数组就可以解决问题,我们关心的是最后一次外循环的值,中间过程我们并不需要知道,所以可以进行空间上的优化。

二维数组中我们进行的操作实际上就是在每个物品的基础上,遍历所有可能的背包空间,如果放得下去就比较放与不放对价值的影响,转换为一维的时候,其实比较的就是在上一次操作的情况下,哪些还可以放得下这个物品,放得下的就进行比较。所以也就是要对w[i]到wei的范围进行更新,转换为代码就是下面这个形式,转换后dp数组的含义就变成了容量不超过j时的最大价值了,省略了前几个物品这一项。

for(int i=0;i=w[i];j--)
	{
		dp[j]=max(dp[j],dp[j-w[i]]+pri[i]);
	}
}
cout<

这里还需要注意,01背包的一维数组表示,更新的时候是逆序的,从代码不难看出,对一个值的更新是基于这个值左边的值进行更新的,如果是顺序更新,那么在更新时一定会先更新左边的值,从而使得后更新的值有可能基础被更新过了,而逆序更新就可以避免这个问题。也可以这么说,因为01背包里面每个物品只能放一次,如果顺序更新,那么假设状态1已经放了一个物品了,那么基于状态1的状态2如果需要更新,那么不就变成了放两个物品了么,这不就出错了,所以应该需要逆序更新。

这里举例子都是举的求最大值,这时需要把数组初始化为0,之后每一次都选取最大值即可。而当要求的是最小值时,则应该把数组初始化为很大的数,每次选取最小值。dp[N][wei]就是要求的最值。

二、完全背包

完全背包问题是在01背包问题上进行的延伸,01背包每次物品的数目只能是0或者1,而完全背包问题就脱离了这个限制,题目一般类似:“在一定数目物品内,挑选总重量不超过一定数目的物品,其中每个物品可以选多次,求背包内物品价值的最大值或者最小值”。这里就需要对上面的模板进行一定的修改了。

其实最笨的办法是在01背包的基础上再增加一层循环,用于记录增加的件数

for(int i=0;i

这种方法比较好懂,但是时间复杂度太大,需要进行优化,这里直接在01背包一维的基础上进行一个优化,其实这两种背包问题都是相通的,区别就在于01不可以放多个而完全背包可以放多个,在前面讲一维化的地方说过,之所以逆序更新是防止放两个的情况出现,那么这里完全背包就正好利用了这一点,把01背包的逆序更新换成顺序更新,就可以解决放几个的问题了,把所有还可以放的状态都更新一遍,从左向右进行更新,如果前面的状态就已经放得下一个了,那么基于这个状态的另一个状态还可以再放一个,所以就基于上个状态继续更新,因而是正序更新。具体表示为,当背包空间为j时,最大价值等于当前值和加上一个当前物品增加价值之后中更大的值。代码如下

for(int i=0;i

所以在记忆这两种背包问题的时候,只需要理解好更新顺序的问题,模板都是一样的,只需要根据顺序进行修改就好了。

三、分组背包

分组背包问题是在01背包的基础上又进行了延伸,01背包问题中每个物品要么选要么不选,在分组背包中,将一系列物品分成几组,一次在一个小组中选择一个或者不选,其实01背包是一种特殊情况下的分组背包,即每个组只有一个物品时的分组背包,我们依然可以沿用01背包的解题思路,只不过进行一下加工,这里直接取一维情况的dp数组,其实我们可以这样认为,每个组既然只能选一个,我们可以先限定一个组别,之后在这个组别中,在总重量允许的条件下,看当前重量下是不是放得下这个组中的物品,放得下就选取价值的更大值。由于每次选择的都是最大值而且是在这一个组中进行的操作,所以保证了一个组中只选择了一个物品。由于分组背包是01背包延伸来的,所以依然是用逆序遍历来实现一维数组的dp。

模板类似下面的代码

for(int i=1;i<=N;i++)//第几组
			for(int j=M;j>=1;j--)//允许的重量 
				for(int k=1;k<=这组的物品数;k++)//每组选物品 
					if(物品的重量<=j)
						dp[j]=max(dp[j],dp[j-物品的重量]+val[i][k]);

四、多重背包

多重背包问题的思路跟完全背包的思路非常类似,只是每种物品的取值是有限制的,因为每件物品的数量是有限制的。

多重背包一般采取转化为01背包的方法,将每个物品按照2的幂次分成几个物品,这样就变成了01背包。把第i种物品换成n[i]件01背包中的物品。考虑二进制的思想,考虑把第i种物品换成若干件物品,使得原问题中第i种物品可取的每种策略——取0…n[i]件——均能等价于取若干件代换以后的物品。另外,取超过n[i]件的策略必不能出现。分成的这几件物品的系数和为n[i],表明不可能取多于n[i]件的第i种物品。这里一般都会用到二进制优化来减少时间复杂度,不然的话三层循环暴力确实有些吃不消。

模板如下

for(int j=1;j<=num[i];j*=2)//二进制优化 
{
	for(int k=n;k>=j*i;k--)//转换为01背包,所以需要逆序更新 
	{
		if(dp[k-i*j]==1)
			dp[k]=1;
	}
		num[i]-=j;//剩余数量进行更新 
}
if(num[i]*i!=0)//对于不能进行二进制更新的部分直接当做一个物品处理 
{
	for(int k=n;k>=num[i]*i;k--)//对于剩下的部分应该也遍历一遍 
	{
		if(dp[k-num[i]*i])
			dp[k]=1;
	}
}

五、总结

总的来说,这四类背包问题的关系大致如下:
01背包:每个物品只有一个,要么选要么不选
完全背包:在01背包基础上解除了只有一个的限制,每个物品随便选
分组背包:嵌套的01背包,每组要么选一个要么不选,每组里面的物品只能选一个,也可以理解为最大数量为1的要么选要么不选
多重背包:在完全背包上加了数量的限制,依然是随便选,但不能超过限制。

结题思路
01背包:逆序更新
完全背包:顺序更新
这两个是基础,理解好原理就不难区分
分组背包:01背包的基础上,再套一层循环来检验每一组的物品
多重背包:二进制优化后转化为01背包

你可能感兴趣的:(动态规划,笔记总结)