代码随想录算法训练营第42天 || 01背包问题 || 416. 分割等和子集
01背包问题
对于这么多背包问题,面试只需要掌握01背包和完全背包基本足够,最多再了解一下多重背包即可。其他很多都是竞赛级别难度。
而这些背包问题中,01背包又是最基础且最重要的。
01背包
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。**每件物品只能用一次**,求解将哪些物品装入背包里物品价值总和最大。
暴力解法
每个物品只有两种状态取或者不取,所以我们可以使用回溯法搜索所有的情况,时间复杂度为 O ( 2 n ) O(2^n) O(2n)
二维dp数组动规法
五部曲
-
确定dp数组下标及其含义
dp[i][j]
表示从下标0-i的物品中任意取,放进容量为j的背包中的最大价值 -
确定递推公式
由dp数组的下标含义,我们可以想到两个方向来推
dp[i][j]
- 不放物品i:此时最大价值为:
dp[i-1][j]
- 放物品i:此时最大价值,我们可以拆分成两部分物品i的价值
value[i]
和除去物品i的价值dp[i-1][j-weigh[i]]
(也就是容量减去i重量的最大价值)
由此,我们不难得到递推公式:
dp[i][j] = Integer.max(dp[i-1][j],dp[i-1][j-weigh[i]]);
- 不放物品i:此时最大价值为:
-
初始化dp数组
我们由递推公式可以发现,当前位置的值是有左上方或正上方推来的。所以我们一开始可以先初始化边缘位置的数值。其余位置不用管,因为我们只会比较左上方和正上方的结果,与自身无关。与之前写的一些动规和不断更新自身并比较自身的题目不同。
- 对于
j = 0
这一列,不难得出dp[i][0] = 0
。 - 对于
i = 0
这一行,只要容量足够放 i 物品 (j >= weigh[i]),dp[0][i] = value[i]
,否则赋值为0
- 对于
-
遍历顺序
背包问题的遍历顺序是非常讲究的,这里追究起来比推导公式还要复杂。
不过对于本题的01背包问题,其实先遍历物品还是背包都可以,因为递推公式用到的都是在左(正)上方,没涉及更复杂的情况。
-
举例推导dp数组
真正写代码的时候,建议先手动模拟一下,再写代码!
public class test {
public static void main(String[] args) {
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int bagSize = 4;
testWeightBagProblem(weight, value, bagSize);
}
/**
* 动态规划获得结果
*
* @param weight 物品的重量
* @param value 物品的价值
* @param bagSize 背包的容量
*/
public static void testWeightBagProblem(int[] weight, int[] value, int bagSize) {
int m = weight.length;
int[][] dp = new int[m][bagSize + 1];
//初始化操作
for (int i = 1; i <= m; i++) {
dp[0][i] = i >= weight[0] ? value[0] : 0;
}
for (int i = 1; i < m; i++) {
for (int j = 1; j <= bagSize; j++) {
dp[i][j] = Integer.max(dp[i - 1][j], j - weight[i] > 0 ? dp[i - 1][j - weight[i]] + value[i] : 0);
System.out.println(i + " " + j + " " + dp[i][j]);
}
}
}
}
一维滚动数组
压缩二维dp数组到一维,我们可以通过覆盖的方式,将dp[i]
层覆盖到dp[i-1]
上,此时得到的dp[j]
,其中j
表示容量,dp[j]
则表示从下标0-i(循环递增i)物品中任意取,放进容量j的背包的最大价值。
动规五部曲
-
确定dp数组及其下标含义
dp[j] 表示:容量为j的背包中,所背的物品价值最大可以为dp[j]
-
确定递推公式
与二维类似,放与不放的比较谈论
- 不放第i个物品:dp[j](上一状态)
- 放第i个物品:
dp[i-weight[i]]+value[i]
所以,可得递推公式:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
-
初始化dp数组
dp[0] = 0无异议,但其余部分呢?
观察递推公式,我们发现比较的是上一状态的dp[j] 和 放第 i 个物品的情况
所以我们可以初始化<=0的数均可,这样就不会影响下一阶段的比较
因为数组默认初始化为0,所以这一步初始化我们可以不体现。
-
一维dp数组遍历顺序
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]); } }
- 先遍历物品,再遍历背包
- 因为这样才可以一层层替换,如果反过来其实整个递推公式都没有意义了,上一状态无法使用
- 倒叙遍历背包容量
- 这里有别于此前二维遍历,二维遍历正序倒叙均可,因为它只与上一层有关,本层怎么遍历都不影响
- 如果正序遍历,那么递推公式中的加入i物品的
dp[j - weight[i]]
就是已经加过i物品的状态,因为正序遍历这个下标肯定在当前下标的左边,然后我们又+value[i]
,显然重复加入了。
- 先遍历物品,再遍历背包
-
举例推导dp数组
一维dp,分别用物品0,物品1,物品2 来遍历背包,最终得到结果如下:
代码:
public class test {
public static void main(String[] args) {
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int bagWight = 4;
testWeightBagProblem(weight, value, bagWight);
}
public static void testWeightBagProblem(int[] weight, int[] value, int bagWeight) {
int[] dp = new int[bagWeight + 1];
//初始化为0,没体现
for (int i = 0; i < value.length; i++) {
for (int j = bagWeight; j > 0; j--) {
if (j - weight[i] >= 0)
dp[j] = Integer.max(dp[j], dp[j - weight[i]] + value[i]);
System.out.println(i + " " + j + " " + dp[j]);
}
}
}
}
面试问题
- 二维实现—>for循环可不可以颠倒
- 一维实现—>for循环可不可以颠倒顺序—>为什么要逆序遍历?能不能正序?—>为什么一维正逆序均可?
416. 分割等和子集
题目介绍:
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
个人思路
在背包容量、物品价值、物品重量的对应关系上没能处理好,不能得到一个合适的01背包模型。
题解解析
本题是01背包问题,因为每个数字只能用一次
要确定的四点:
- 背包的体积为sum/2
- 背包要放的元素 重量为 元素的数值,价值也为元素的数值(很关键)
- 背包正好装满,说明找到了总和为sum/2的子集
- 背包的每一个元素都是不可重复放入的
动规五部曲
-
确定dp数组及其下标含义
dp[j]
表示容量为j的背包所能装的最大价值,for循环物品i表示放不放物品i -
确定递推公式
dp[j] = Integer(dp[j],dp[j-nums[i]+nums[i]);
物品的重量和价值都是
nums[i]
-
初始化dp数组
默认初始化为0 即可,隐式初始化
-
遍历顺序
一维滚动数组:
- 先遍历物品,再遍历背包
- 背包容量逆序遍历(避免重复加入)
-
打印dp数组检验
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for (int i = 0; i < nums.length; i++)
sum += nums[i];
if (sum % 2 == 1)
return false;
sum /= 2;
int[] dp = new int[sum + 1];
//背包容量 == 总和一半
for (int i = 0; i < nums.length; i++) {
for (int j = sum; j > 0; j--) {
if (j - nums[i] >= 0)
dp[j] = Integer.max(dp[j], dp[j - nums[i]] + nums[i]);
if (dp[j] == sum)//过程中,背包能装满/价值满足就返回结果
return true;
}
}
//遍历完所有情况都不能装满背包/价值满足
return false;
}
}
回顾本题,关键点在于如何确定物品的价值和重量及背包的容量,从而得到一个合适的01背包模型。
- 物品重量和物品数值相等、背包容量与目标数值相等?
- 为了能正确放入物品,避免放入过大的物品,导致后序的背包遍历都无意义
- 物品价值 == 数值?
- 为了检验满足背包放满的条件,用价值"放满"来等价于背包放满