1049. 最后一块石头的重量 II
中等
提示
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
难点:本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了。很难想到这个点啊。其实就是分成尽量相同的两堆,互相碰撞,就能剩下最小了。其实就是设bagSize为sum/2,让其中一堆石头尽量接近bagSize。
class Solution {
public int lastStoneWeightII(int[] stones) {
int sum = 0;
for (int i : stones) {
sum += i;
}
int target = sum >> 1; // 右移1,不是2
int dp[] = new int[target + 1]; // dp[j]表示容量为j时能获得的最大价值
for (int i = 0; i < stones.length; i++) { // 遍历物品
for (int j = dp.length - 1; j >= stones[i]; j--) { // 遍历容量,注意这里的终止条件真的很精妙,二维的还需要if else判断一下,这里直接省略了
dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
int result = sum - 2 * dp[target];
return result;
}
}
494. 目标和
中等
给你一个非负整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
难点:
这道题目咋眼一看和动态规划背包啥的也没啥关系。
本题要如何使表达式结果为target,
既然为target,那么就一定有 left组合 - right组合 = target。
left + right = sum,而sum是固定的。right = sum - left
公式来了, left - (sum - left) = target 推导出 left = (target + sum)/2 。
target是固定的,sum是固定的,left就可以求出来。
此时问题就是在集合nums中找出和为left的组合。
网友思路:本质是选数字放在包包里过程
网友①
这里,我们可以把状态定义为dp【i】【j】,表示用数组中前i个元素组成和为j的方案数。那么状态转移方程就是:
dp【i】【j】 = dp【i-1】[j-nums【i】] + dp【i-1】[j+nums【i】]
这个方程的意思是,如果我们要用前i个元素组成和为j的方案数,那么有两种选择:第i个元素取正号或者取负号。如果取正号,那么前i-1个元素就要组成和为j-nums【i】的方案数;如果取负号,那么前i-1个元素就要组成和为j+nums【i】的方案数。所以两种选择的方案数相加就是dp【i】【j】。
但是这样定义状态会导致空间复杂度过高,因为我们需要一个二维数组来存储所有可能的状态。所以我们可以对问题进行一些变换,把它转化为一个背包问题。
我们可以把数组中所有取正号的元素看作一个集合P,所有取负号的元素看作一个集合N。那么有:
sum§ - sum(N) = target
sum§ + sum(N) = sum(nums)
两式相加得:
sum§ = (target + sum(nums)) / 2
也就是说,我们只需要找到有多少种方法可以从数组中选出若干个元素使得它们的和等于(target + sum(nums)) / 2即可。这就变成了一个经典的01背包问题。
所以我们可以把状态定义为dp【j】,表示用数组中若干个元素组成和为j的方案数。那么状态转移方程就是:
dp【j】 = dp【j】 + dp[j - nums【i】]
这个方程的意思是,如果我们要用若干个元素组成和为j的方案数,那么有两种选择:不选第i个元素或者选第i个元素。如果不选第i个元素,那么原来已经有多少种方案数就不变;如果选第i个元素,那么剩下要组成和为j - nums【i】 的方案数就等于dp[j - nums【i】]。所以两种选择相加就是dp【j】。
但是在实现这个状态转移方程时,有一个细节需要注意:由于每次更新dp【j】都依赖于之前计算过得dp值(也就是说当前行依赖于上一行),所以我们必须从后往前遍历更新dp值(也就是说从右往左更新),否则会覆盖掉之前需要用到得值。
最后返回dp【bag_size】即可。
网友②:
这道题感觉没讲清楚呀。。建议不懂的同学自己先用二维数组来做,比较好理解,理解了之后再用一维数组。
- 含义:dp【i】【j】:从下标为【0…i】的物品里任取,填满j这么⼤容积的包,有dp【i】【j】种⽅法
2. 递推式:dp【i】【j】 = dp【i-1】【j】 + dp【i-1】[j-nums【i】]
dp【i-1】【j】是不将物品i放入背包的方式数,dp【i-1】[j-nums【i】]是将物品i放入背包的方式数
3. 初始化:dp【0】【0】 = 1 表示装满容量为0的背包,有1种⽅法,就是装0件物品。
如果nums【0】在范围内的话,dp【0】[nums【0】] = 1
其他全为0
4. 计算顺序:顺序,行优先
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for (int num : nums) {
sum += num;
}
if (Math.abs(target) > sum) return 0;
if ((target + sum) % 2 == 1) return 0;
int bagSize = (target + sum) / 2; // x - (sum - x) = target
int[] dp = new int[bagSize + 1]; // dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法
dp[0] = 1;
for (int i = 0; i < nums.length; i++) { // 遍历物品(数字)
for (int j = bagSize; j >= nums[i]; j--) { // 遍历背包
dp[j] = dp[j] + dp[j - nums[i]];
}
}
return dp[bagSize];
}
}
总结:
474. 一和零
中等
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
分析:本题,物品就是strs里的字符串,背包容量就是题目描述中的m和n。
// 这个题建议先看看官方(或其他人)的三维数组的题解
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) {
// 统计每个字符串里0 和 1的数量
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];
}
}