前言
代码随想录算法训练营day43
一、Leetcode 1049. 最后一块石头的重量 II
1.题目
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
1. 如果 x == y,那么两块石头都会被完全粉碎; 2. 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
示例 1:
输入:stones = [2,7,4,1,8,1] 输出:1 解释: 组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1], 组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1], 组合 2 和 1,得到 1,所以数组转化为 [1,1,1], 组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。
示例 2:
输入:stones = [31,26,33,21,40] 输出:5
提示:
1. 1 <= stones.length <= 30 2. 1 <= stones[i] <= 100
来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/last-stone-weight-ii
2.解题思路
方法一:动态规划
记石头的总重量为 sumsum,ki=−1ki=−1 的石头的重量之和为 negneg,则其余 ki=1ki=1 的石头的重量之和为 sum−negsum−neg。则有
∑i=0n−1ki⋅stonesi=(sum−neg)−neg=sum−2⋅negi=0∑n−1ki⋅stonesi=(sum−neg)−neg=sum−2⋅neg
要使最后一块石头的重量尽可能地小,negneg 需要在不超过 ⌊sum/2⌋⌊sum/2⌋ 的前提下尽可能地大。因此本问题可以看作是背包容量为 ⌊sum/2⌋⌊sum/2⌋,物品重量和价值均为 stonesistonesi 的 0-1 背包问题。
对于该问题,定义二维布尔数组 dpdp,其中 dp[i+1][j]dp[i+1][j] 表示前 ii 个石头能否凑出重量 jj。特别地,dp[0][]dp[0][] 为不选任何石头的状态,因此除了 dp[0][0]dp[0][0] 为真,其余 dp[0][j]dp[0][j] 全为假。
对于第 ii 个石头,考虑凑出重量 jj:
1. 若 j<stones[i]j<stones[i],则不能选第 ii 个石头,此时有 dp[i+1][j]=dp[i][j]dp[i+1][j]=dp[i][j]; 2. 若 j≥stones[i]j≥stones[i],存在选或不选两种决策,不选时有 dp[i+1][j]=dp[i][j]dp[i+1][j]=dp[i][j],选时需要考虑能否凑出重量 j−stones[i]j−stones[i],即 dp[i+1][j]=dp[i][j−stones[i]]dp[i+1][j]=dp[i][j−stones[i]]。若二者均为假则 dp[i+1][j]dp[i+1][j] 为假,否则 dp[i+1][j]dp[i+1][j] 为真。
因此状态转移方程如下:
dp[i+1][j]={dp[i][j],j
其中 ∨∨ 表示逻辑或运算。求出 dp[n][]dp[n][] 后,所有为真的 dp[n][j]dp[n][j] 中,最大的 jj 即为 negneg 能取到的最大值。代入 sum−2⋅negsum−2⋅neg 中即得到最后一块石头的最小重量。
3.代码实现
```java class Solution { public int lastStoneWeightII(int[] stones) { int sum = 0; for (int weight : stones) { sum += weight; } int n = stones.length, m = sum / 2; boolean[][] dp = new boolean[n + 1][m + 1]; dp[0][0] = true; for (int i = 0; i < n; ++i) { for (int j = 0; j <= m; ++j) { if (j < stones[i]) { dp[i + 1][j] = dp[i][j]; } else { dp[i + 1][j] = dp[i][j] || dp[i][j - stones[i]]; } } } for (int j = m;; --j) { if (dp[n][j]) { return sum - 2 * j; } } } } ```
二、Leetcode 494. 目标和
1.题目
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
示例 1:
输入:nums = [1,1,1,1,1], target = 3 输出:5 解释:一共有 5 种方法让最终目标和为 3 。 -1 + 1 + 1 + 1 + 1 = 3 +1 - 1 + 1 + 1 + 1 = 3 +1 + 1 - 1 + 1 + 1 = 3 +1 + 1 + 1 - 1 + 1 = 3 +1 + 1 + 1 + 1 - 1 = 3
示例 2:
输入:nums = [1], target = 1 输出:1
提示:
1. 1 <= nums.length <= 20 2. 0 <= nums[i] <= 1000 3. 0 <= sum(nums[i]) <= 1000 4. -1000 <= target <= 1000
来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/target-sum
2.解题思路
方法一:回溯
数组 numsnums 的每个元素都可以添加符号 ++ 或 --,因此每个元素有 22 种添加符号的方法,nn 个数共有 2n2n 种添加符号的方法,对应 2n2n 种不同的表达式。当 nn 个元素都添加符号之后,即得到一种表达式,如果表达式的结果等于目标数 targettarget,则该表达式即为符合要求的表达式。
可以使用回溯的方法遍历所有的表达式,回溯过程中维护一个计数器 countcount,当遇到一种表达式的结果等于目标数 targettarget 时,将 countcount 的值加 11。遍历完所有的表达式之后,即可得到结果等于目标数 targettarget 的表达式的数目。
3.代码实现
```java class Solution { int count = 0;
1. public int findTargetSumWays(int[] nums, int target) { 2. backtrack(nums, target, 0, 0); 3. return count; 4. } 5. 6. public void backtrack(int[] nums, int target, int index, int sum) { 7. if (index == nums.length) { 8. if (sum == target) { 9. count++; 10. } 11. } else { 12. backtrack(nums, target, index + 1, sum + nums[index]); 13. backtrack(nums, target, index + 1, sum - nums[index]); 14. } 15. }
}
```
三、Leetcode 474.一和零
1.题目
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
示例 1:
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3 输出:4 解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。 其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
示例 2:
输入:strs = ["10", "0", "1"], m = 1, n = 1 输出:2 解释:最大的子集是 {"0", "1"} ,所以答案是 2 。
提示:
1. 1 <= strs.length <= 600 2. 1 <= strs[i].length <= 100 3. strs[i] 仅由 '0' 和 '1' 组成 4. 1 <= m, n <= 100
来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/ones-and-zeroes
2.解题思路
方法一:动态规划
这道题和经典的背包问题非常相似,但是和经典的背包问题只有一种容量不同,这道题有两种容量,即选取的字符串子集中的 00 和 11 的数量上限。
经典的背包问题可以使用二维动态规划求解,两个维度分别是物品和容量。这道题有两种容量,因此需要使用三维动态规划求解,三个维度分别是字符串、00 的容量和 11 的容量。
定义三维数组 dpdp,其中 dp[i][j][k]dp[i][j][k] 表示在前 ii 个字符串中,使用 jj 个 00 和 kk 个 11 的情况下最多可以得到的字符串数量。假设数组 strstr 的长度为 ll,则最终答案为 dp[l][m][n]dp[l][m][n]。
当没有任何字符串可以使用时,可以得到的字符串数量只能是 00,因此动态规划的边界条件是:当 i=0i=0 时,对任意 0≤j≤m0≤j≤m 和 0≤k≤n0≤k≤n,都有 dp[i][j][k]=0dp[i][j][k]=0。
当 1≤i≤l1≤i≤l 时,对于 strsstrs 中的第 ii 个字符串(计数从 11 开始),首先遍历该字符串得到其中的 00 和 11 的数量,分别记为 zeroszeros 和 onesones,然后对于 0≤j≤m0≤j≤m 和 0≤k≤n0≤k≤n,计算 dp[i][j][k]dp[i][j][k] 的值。
当 00 和 11 的容量分别是 jj 和 kk 时,考虑以下两种情况:
1. 如果 j<zerosj<zeros 或 k<onesk<ones,则不能选第 ii 个字符串,此时有 dp[i][j][k]=dp[i−1][j][k]dp[i][j][k]=dp[i−1][j][k]; 2. 3. 如果 j≥zerosj≥zeros 且 k≥onesk≥ones,则如果不选第 ii 个字符串,有 dp[i][j][k]=dp[i−1][j][k]dp[i][j][k]=dp[i−1][j][k],如果选第 ii 个字符串,有 dp[i][j][k]=dp[i−1][j−zeros][k−ones]+1dp[i][j][k]=dp[i−1][j−zeros][k−ones]+1,dp[i][j][k]dp[i][j][k] 的值应取上面两项中的最大值。
因此状态转移方程如下:
dp[i][j][k]={dp[i−1][j][k],j
最终得到 dp[l][m][n]dp[l][m][n] 的值即为答案。
由此可以得到空间复杂度为 O(lmn)O(lmn) 的实现。
3.代码实现
```java class Solution { public int findMaxForm(String[] strs, int m, int n) { int length = strs.length; int[][][] dp = new int[length + 1][m + 1][n + 1]; for (int i = 1; i <= length; i++) { int[] zerosOnes = getZerosOnes(strs[i - 1]); int zeros = zerosOnes[0], ones = zerosOnes[1]; for (int j = 0; j <= m; j++) { for (int k = 0; k <= n; k++) { dp[i][j][k] = dp[i - 1][j][k]; if (j >= zeros && k >= ones) { dp[i][j][k] = Math.max(dp[i][j][k], dp[i - 1][j - zeros][k - ones] + 1); } } } } return dp[length][m][n]; }
1. public int[] getZerosOnes(String str) { 2. int[] zerosOnes = new int[2]; 3. int length = str.length(); 4. for (int i = 0; i < length; i++) { 5. zerosOnes[str.charAt(i) - '0']++; 6. } 7. return zerosOnes; 8. }
}
```