1049. 最后一块石头的重量 II - 力扣(LeetCode)
有一堆石头,每块石头的重量都是正整数。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。
示例:
- 输入:[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],这就是最优值。
提示:
-
1 <= stones.length <= 30
-
1 <= stones[i] <= 1000
public int lastStoneWeightII(int[] stones) {
if (stones.length == 1) {
return stones[0];
}
int sum = 0;
for (int i = 0; i < stones.length; i++) {
sum += stones[i];
}
int target = sum / 2;
int[] dp = new int[target + 1];
for (int i = 0; i < stones.length; i++) {
//用大小为target的背包去装物品, j >= nums[i]是保证背包容量够用
for (int j = target; j >= stones[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
return sum - dp[target] - dp[target];
}
解题:
类似于416. 分割等和子集 - 力扣(LeetCode),我们本题是去磨石头,每次拿俩块磨,所以我们与上面这题类似,要找到sum / 2,找到最大的一半,然后用sum - dp[target] - dp[target]去获取最后剩下的那个石头。这个石头就是剩下的最小的,因为我们已经尽可能的磨了,取的是和的一半,就算没有取满,但是剩下的依旧很小。
|----------|-------------------------------------------|
| 要点 | 说明 |
| 问题转化 | 石头碰撞 → 分成两堆使差最小 → 0/1背包 |
| 背包容量 | target = sum / 2(向下取整) |
| 倒序遍历 | j 从大到小,确保每个物品只用一次 |
| 状态定义 | dp[j] = 容量j的背包能装的最大重量 |
| 转移方程 | dp[j] = max(dp[j], dp[j-stone] + stone) |
494. 目标和 - 力扣(LeetCode)
给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
示例:
- 输入:nums: [1, 1, 1, 1, 1], S: 3
- 输出:5
解释:
- -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
一共有5种方法让最终目标和为3。
提示:
-
数组非空,且长度不会超过 20 。
-
初始的数组的和不会超过 1000 。
-
保证返回的最终结果能被 32 位整数存下。
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for (int num : nums) {
sum += num;
}// 边界条件1:目标和绝对值超过总和,不可能 if (Math.abs(target) > sum) { return 0; } // 边界条件2:(target + sum) 必须是偶数,且 target 不能超过 sum if ((target + sum) % 2 != 0 || target > sum) { return 0; } // 转化为背包容量 int left = (target + sum) / 2; // dp[j] 表示和为 j 的方案数 int[] dp = new int[left + 1]; dp[0] = 1; // 和为 0 的方案有 1 种:什么都不选 // 0/1 背包:遍历物品 for (int i = 0; i < nums.length; i++) { // 倒序遍历容量(0/1背包防止重复计算) for (int j = left; j >= nums[i]; j--) { // 状态转移:不选 nums[i] 的方案数 + 选 nums[i] 的方案数 dp[j] = dp[j] + dp[j - nums[i]]; } } return dp[left];}
解题:
思路:分为正数集合和负数集合,俩个集合相加要等于目标值,这里记作left - right = target, 并且 left + right = sum, 所以left - (sum - left) = target,所以left = (target + sum)/ 2。这样我们就确定了我们要找的值,并且如果(target + sum)/ 2无法整除,就说明不能找到这样的集合,直接返回即可。
根据题目意思,我们的dp数组就是容量为i的背包有多少种填满方式,递推公式类似于机器人走到右下角的题目(不同路径) 。并且需要注意的是,我们的dp[0]是初始化为1,因为后续我们递推需要有值,而不是0,若是0的话后续都是0.
474. 一和零 - 力扣(LeetCode)
给你一个二进制字符串数组 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 <= strs.length <= 600
-
1 <= strs[i].length <= 100
-
strs[i] 仅由 '0' 和 '1' 组成
-
1 <= m, n <= 100
public int findMaxForm(String[] strs, int m, int n) {
int[][] dp = new int[m + 1][n + 1];
for (String str : strs) {
int[] count = new int[2];
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) == '0') {
count[0]++;
} else {
count[1]++;
}
}
for (int i = m; i >= count[0]; i--) {
for (int j = n; j >= count[1]; j--) {
dp[i][j] = Math.max(dp[i][j], dp[i - count[0]][j - count[1]] + 1);
}
}
}
return dp[m][n];
}
解题:
字符串数组中,每一个元素是一个一个字符串,我们需要的是这个字符串中0和1的数量,所以后续我们需要统计每一个字符串的相关值。定义dp[i][j],表示分别最大存储 i 和 j 的数量,这里去表示0和1。我们的递推公式是类似于之前的一维递推dp[i][j] = Math.max(dp[i][j], dp[i - count[0]][j - count[1]] + 1),区别在于这里有俩个维度,所以计算的时候,俩个都要考虑。遍历顺序依旧是倒序遍历,保证元素只被使用一次。