代码随想录算法训练营第四十二天 | LeetCode 1049. 最后一块石头的重量 II、494. 目标和、474. 一和零
文章链接:最后一块石头的重量 II目标和一和零
视频链接:最后一块石头的重量 II目标和一和零
1. LeetCode 1049. 最后一块石头的重量 II
1.1 思路
- 这题不要被题目的示例给带偏,其实就是看看我们能不能把石头尽量分成两堆,比如示例的总和为 23,看看能不能分成一半就是 11,另一半就是 12 了,一相撞就是剩下 1 了。即尽量分成重量总和近似相等的两堆,就是想到本题的关键了,这么看和416. 分割等和子集还是很像的,区别就是那题是能分成子集相等的就是 true 反之 false,本题是尽量凑,凑不成就相撞所剩下的重量也是最小的。
- dp 数组的下标及其含义:在 01 背包中,背包容量为 j 所能带的最大价值为 dp[j]。但本题的重量/容量也是它的价值,所以这里的最大价值也是它的最大重量,背包容量为 j 所能带的最大重量为 dp[j]
- 递推公式:在 01 背包中,dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i]),前者是不放物品 i,后者是放物品 i。在本题中 dp[j]=Math.max(dp[j],dp[j-stones[i]]+stones[i]),前者是不放石头 i,后者是放石头 i
- dp 数组的初始化:dp[0]=0,容量 0 的背包能放的重量也是 0。其他下标初始化为 0,因为我们的递推公式是通过别的值覆盖 dp[j] 的,因此不能初始化太大,为 0 最正确。我们这里定义 dp 数组的大小就是,先求 sum,target=sum/2,dp 数组的大小就是 target+1,因为我们要凑容量的一半
- 遍历顺序:两层 for循环,for(int i=0;i<stones.length;i++)for(int j=target;j>=stones[i];j–)先物品后背包,然后背包这一层从后往前遍历,这里为什么j>=stones[i] 是因为如果背包容量比石头重量还小再遍历就没有意义了
- 打印 dp 数组:用于 debug
- 最后 return sum-2*dp[target]
1.2 代码
class Solution { public int lastStoneWeightII(int[] stones) { int sum = 0; for (int i : stones) { sum += i; } int target = sum >> 1; //初始化dp数组 int[] dp = new int[target + 1]; for (int i = 0; i < stones.length; i++) { //采用倒序 for (int j = target; j >= stones[i]; j--) { //两种情况,要么放,要么不放 dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]); } } return sum - 2 * dp[target]; } }
2. LeetCode 494. 目标和
2.1 思路
- 我们可以在集合中加入“+”“-”变为一个表达式,使其等于我们的 target。关于放“+”还是“-”就是把这个集合分成了两个集合,放加法的是一个集合,放减法的是一个集合。本题和416. 分割等和子集也是一个集合里分两个子集还有1049. 最后一块石头的重量 II也是一个集合里分出两个子集。本题中我们加法用 left,减法用 right,加法和减法的集合就是原集合的总和 sum,要求加法集合 left-减法集合 right=target,一相加就是 2left=sum+target => 即 left=(sum+target)/2。举例 [1,1,1,1,1] 即 sum=5,题目的 target=3,因此 left=(5+3)/2,就是 4 个用加法,1 个用减法,我们求出 left 集合剩下的就是 right 集合了。当然也是有用例是求不出 left 集合的,比如如果上面的是 target=2,那我们的 left 和 right 都找不到一种匹配的集合等于 target,因此如果判断出 (target+sum)%2!=0 就最终输出 0。此时问题转化为,求出 left 集合,原集合里所有元素装满这个 left 集合有多少种方法,就能找到符合题目条件的多少种组合,这就是背包问题了,left 就是背包容量,原集合就是我们的物品
- 纯 01 背包就是问装满这个背包的最大价值,416. 分割等和子集就是能不能装满这个背包,满了就 true 否则就 false,1049. 最后一块石头的重量 II结束尽量去装,能装多少装多少,本题就是给我们一个容量,问有多少种方式能把背包装满
- dp 数组及其下标的含义:dp[j] 装满背包容量为 j 有 dp[j] 种方法。bagSize=(target+sum)/2
- 递推公式:dp[j]+=dp[j-nums[i]]
- dp 数组的初始化:在 01 背包中 dp[0] 初始化为 0,即背包容量为 0 的背包最大价值是 0,但是本题,dp[0] 应该初始化为 1,因为我们的 dp[j] 是通过前面累加的,如果初始化为 0,后面就一直是 0 了
- 遍历顺序:01 背包中先物品再背包,因此 for(int i=0;i<nums.length;i++)for(int j=bagSize;j>=nums[i];j–)第二层倒序遍历,然后 dp[j]+=dp[j-nums[i]]
- 打印 dp 数组:用于 debug
2.2 代码
class Solution { public int findTargetSumWays(int[] nums, int target) { int sum = 0; for (int i = 0; i < nums.length; i++) sum += nums[i]; //如果target过大 sum将无法满足 if ( target < 0 && sum < -target) return 0; if ((target + sum) % 2 != 0) return 0; int size = (target + sum) / 2; if(size < 0) size = -size; int[] dp = new int[size + 1]; dp[0] = 1; for (int i = 0; i < nums.length; i++) { for (int j = size; j >= nums[i]; j--) { dp[j] += dp[j - nums[i]]; } } return dp[size]; } }
3. LeetCode 474. 一和零
3.1 思路
- 本题让我们从题目的集合中找出一个最多元素的子集,满足有 m 个 0,n 个 1。可以把 m 个 0 和 n 个 1 理解为一个背包,有两个维度,最多有 m 个 0 和 n 个 1,而集合里的元素就是物品。但这题不是多重背包,还是 01 背包,每个物品只能是使用 1 次,只是有两个维度。而多重背包是会限制物品的个数。
- 纯 01 背包就是问装满这个背包的最大价值,416. 分割等和子集就是能不能装满这个背包,满了就 true 否则就 false,1049. 最后一块石头的重量 II结束尽量去装,能装多少装多少,494. 目标和就是给我们一个容量,问有多少种方式能把背包装满,本题是求装满这个背包最多有多少个物品
- dp 数组及其下标的含义:我们是要装满一个 m 个 0、n 个 1 这么大容器的背包,这个背包里最多有多少个物品,一共 3 个变量,m、n、多少个物品。两个维度 m 个 0、n 个 1,因此定义个二维 dp 数组,含义是最多有 i 个 0 j 个 1 的 strs 的最多有 dp[i][j] 个物品,最终 dp[m][n] 就是我们的结果
- 递推公式:纯 01 背包的公式是 dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i]),本题中我们物品的重量就是 x 个 0 y 个 1,整个背包最大重量是 m 个 0 n 个 1。因此 dp[i-x][j-y]+1,这里减去的 x 和 y 就是物品的重量,+1 就是物品的价值,价值就是个数,我们求的就是最多个数。可以这么理解:放了物品 i,还剩下 j - weight[i] 的容量可以放其他物品,剩下空间能得到的最大价值是 dp[i-1][j-weight[i]] 这句话是引用 01 背包理论基础的,理解一下。因此 dp[i][j]=Math.max(dp[i][j], dp[i-x][j-y]+1)
- dp 数组的初始化:dp[0][0]=0,背包容量为 0 了,最大的物品个数自然也是 0。非 0 下标 dp[i][j] 物品个数不会是负数,因此初始化为 0,就不会在递推时 dp[i][j] 被初始化值覆盖掉
- 遍历顺序:也是"两层"for循环,先物品再背包。先通过增强 for循环遍历 str,每遍历到一个物品就要用 x 和 y 记录有几个 0 和 1。然后就是遍历背包,这里是要遍历 0 和 1,我们是先 0 再 1,不过可以颠倒,但是要倒序遍历, for(int i=m;i>=x;i–)for(int j=n;j>=y;j–)
- 打印 dp 数组:用于 debug
3.2 代码
class Solution { public int findMaxForm(String[] strs, int m, int n) { //dp[i][j]表示i个0和j个1时的最大子集 int[][] dp = new int[m + 1][n + 1]; int oneNum, zeroNum; for (String str : strs) { oneNum = 0; zeroNum = 0; for (char ch : str.toCharArray()) { if (ch == '0') { zeroNum++; } else { oneNum++; } } //倒序遍历 for (int i = m; i >= zeroNum; i--) { for (int j = n; j >= oneNum; j--) { dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1); } } } return dp[m][n]; } }