背包问题可以分为以上几种,在本章将从两个热点方面进行介绍:
- 0-1背包及优化
「力扣」第 416 题:分割等和子集(中等);
「力扣」第 474 题:一和零(中等);
「力扣」第 494 题:目标和(中等);
「力扣」第 879 题:盈利计划(困难);
- 完全背包
「力扣」第 322 题:零钱兑换(中等);
「力扣」第 518 题:零钱兑换 II(中等);
「力扣」第 1449 题:数位成本和为目标值的最大数字(困难)。
一、0-1背包
1、问题定义
有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。
定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。设第 i 件物品体积为w[i],价值为 v[i],根据第 i 件物品是否添加到背包中,可以分两种情况讨论:
- 第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,。
- 第 i 件物品添加到背包中,。
第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。因此,0-1 背包的状态转移方程为:
2、问题分析
我们按照之前的解题思路五部曲进行分析:
(1)确定dp数组以及下标含义
i表示前i件物品,j表示背包容量。dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
(2)确定状态转移方程
①装第i件物品,那么前i-1物品最大重量为j-w[i],最大价值为dp[i-1][j-w[i]];装入物品i后,最大价值为。
②不装第i件物品,最大的价值就是容量为j的背包装前i-1件物品,。
(3)dp数组的初始化
①如果容量j=0,那么背包将装不了任何物品,所以dp[...][0]=0;
②装前0件物品,即没有物品时,容量再大也没有价值,dp[0][...]=0;
(4)确定遍历的顺序
可以看出,有两个遍历的维度:物品与背包重量
那么问题来了,先遍历 物品还是先遍历背包重量呢?
其实都可以!!但是先遍历物品更好理解。
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j]; // 这个是为了展现dp数组里元素的变化
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
为什么也是可以的呢?
要理解递归的本质和递推的方向。
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 递归公式中可以看出dp[i][j]是靠dp[i-1][j]和dp[i - 1][j - weight[i]]推导出来的。
dp[i-1][j]和dp[i - 1][j - weight[i]] 都在dp[i][j]的左上角方向(包括正左和正上两个方向),那么先遍历物品,再遍历背包的过程如图所示:
再来看看先遍历背包,再遍历物品呢,如图:
大家可以看出,虽然两个for循环遍历的次序不同,但是dp[i][j]所需要的数据就是左上角,根本不影响dp[i][j]公式的推导!
但先遍历物品再遍历背包这个顺序更好理解。
其实背包问题里,两个for循环的先后循序是非常有讲究的,理解遍历顺序其实比理解推导公式难多了。
(5)举例检验
建议大家此时自己在纸上推导一遍,看看dp数组里每一个数值是不是这样的。
做动态规划的题目,最好的过程就是自己在纸上举一个例子把对应的dp数组的数值推导一下,然后在动手写代码!
3、空间优化--滚动数组
在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
观察状态转移方程可以知道,前 i 件物品的状态仅与前 i-1 件物品的状态有关,其实可以把dp[i - 1]这一层拷贝到dp[i]上,只用一个一维数组dp[j](一维数组,也可以理解是一个滚动数组)。
这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。
我们依然使用五部曲进行分析:
(1)确定dp数组以及下标含义
j表示背包容量。dp[j] 表示容量为j的背包,价值总和最大是多少。
(2)确定状态转移方程
①装第i件物品,那么前i-1物品最大重量为j-w[i],最大价值为dp[j-w[i]];装入物品i后,最大价值为。
②不装第i件物品,最大的价值就是容量为j的背包装前i-1件物品,。
此时,
(3)dp数组的初始化
如果容量j=0,那么背包将装不了任何物品,所以dp[0]=0;
(4)确定遍历的顺序
这时候,遍历的顺序就很关键了。因为没更新的dp[j]保留了上一层的结果,也就是 dp[i-1][j]的结果,所以,在更新dp[j]时,要用到之前的dp[j-w],它必须是没被更新过的!
当j从小到大遍历时,前面的dp[j-w]不断被更新,导致dp[j]在更新时,用的是已经更新过的dp[j-w],导致同一物体被放进去多次。
因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w],防止将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。
(5)举例检验
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
网友评论