题目介绍
有一堆石头,用整数数组 stones
表示。其中 stones[i]
表示第 i
块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x
和 y
,且 x <= y
。那么粉碎的可能结果如下:
x == y
,那么两块石头都会被完全粉碎;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
个人思路
本题与昨天写的分割相等子集有异曲同工之处,本题石头互相碰撞,我们不要陷入它的模拟状态,跳到上帝模式,我们先将石头分成两堆,想象成两块大石头相互碰撞,这样我们自然能想到要分成尽可能接近的两堆才能得到题解。所以这又回到了01背包问题上了,石头重量和价值都相同,背包容量设置成总重量的一半,最终看能放入的石头的最大价值即可。
动规五部曲
确定dp数组及其下标含义
dp[j]
表示背包容量为j的背包能放入石头的最大价值(for循环遍历石头限制0~i
个石头可放入)
确定递推公式
dp[j] = Integer.max(dp[j], dp[j - stones[i]] + stones[i]);
放与不放第i块石头的比较,前者是不放i石头的上一状态最大价值,后一状态是放i石头的最大价值
初始化dp数组
因为价值都是大于0,所以我们一开始的默认初始化0即可,代码隐式初始化了
遍历顺序确定
打印dp数组检验
代码:
class Solution {
public int lastStoneWeightII(int[] stones) {
//本质上将石堆分成两份,是两份重量尽可能接近
//然后我们发现这道题和昨天写的那道题分割等和子集差不多,这题就是尽可能分割等和子集
//其实就是找到最接近stone总重量的一半
//01背包:石头重量价值都一样,价值用于记录重量;背包容量设置总重量一般即可
int sum = 0;
for (int i = 0; i < stones.length; i++) {
sum += stones[i];
}
int num = sum / 2;
int[] dp = new int[num + 1];
for (int i = 0; i < stones.length; i++) {
for (int j = num; j > 0; j--) {
if (j - stones[i] >= 0)
dp[j] = Integer.max(dp[j], dp[j - stones[i]] + stones[i]);
// System.out.println(i + " " + j + " " + dp[j]);
}
}
return sum - 2 * dp[num];
}
}
题目介绍
给你一个整数数组 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
个人思路及误区
之所以没能解出来,一个大问题就是第一步确定dp数组含义有误,误将本题和之前题相同处理,把dp[j]理解为数字的总和,每次达到这个总和就记录一次结果,结果绕来绕去感觉回到了小回溯的思路,不能解决本题。所以确定dp数组及其下标也是非常重要的。
题解解析
本题有点类似上一题的石头分类,这里是分成 加数 和 减数 两大类,然后运算后得到目标值。
由此,我们可以得到减数num = (sum - target)/ 2;
(这里也可以用加数做运算)这样我们就可以将本题转化成求背包容量为num的01背包问题(每个物品只能放一次)
不过本题与之前的问题不一样,之前问的都是容量为j的背包最多能装多少?–>能不能装满? 本题问的是装满的有几种方法。因此,我们定义的dp数组的含义为:dp[j] 表示:容量 j 的背包有多少中方法装满
一些特殊情况的排除:(不排除可能会影响动规的结果,因为dp[0] = 1
)
(sum - target) % 2 == 1
这种情况也是无解的,我们知道sum可以分成两部分 left 和 right ,其中left - right = target
,这也就意味着两堆数插值target,所以sum - target == 2 * right 必然为偶数动规五部曲
确定dp数组及其下标含义
dp[j] : 背包容量为 j 的背包最多有几种情况装满
一个细节:由于target可以小于0,(num =(sum + target)/ 2 情况下)也就导致num可能小于0,我们要先取绝对值再new数组。
这样没有影响,会对称回去。举个例子:
sum = 100 ,target = -50 ,num = -75 (另一个num就是 25)num = 75其实就是25的对称值。
当然,我们一开始设置 num =(sum - target)/ 2
就不会产生负数情况,真要产生就是上面的不符情况1.
确定递推方法
dp[j] = dp[j-nums[i]]
可以这么理解,还是先从放与不放引入。
当遍历到第 i 个物品,背包容量为 j 时,要知道此时放入 i 情况的最多方法,其实就等价于容量为j - nums[i]
的背包装满的最多方法
注意:如果放入i比背包容量大,就不会刷新此刻的dp数组,保持原来的最大值(if判断)
初始化dp数组
dp[0] = 1
这里不用过于解释为什么一开始容量为0的背包有一种情况装满,如果 = 0 递推公式一直都是0;
这里可能会有疑问,万一数组 [0,0,0,0,0] target = 0怎么办?这样一开始设置为dp[0] = 1,会不会导致最终结果多1
不用担心,1 — 2 — 4 — 8 — 16 — 32 五个过程走过来,+0和-0就是两种情况了,所以就算是[0] target = 0 也可以得到结果 2
所以,这里的初始化也是很重要的
确定遍历顺序
和之前一样,遵循两个规则
打印dp数组检验
代码:
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for (int i = 0; i < nums.length; i++)
sum += nums[i];
//两种不符情况排除
if (sum < target || sum < target * (-1))
return 0;
if ((sum - target) % 2 == 1)
return 0;
int num = (sum - target) / 2;
//dp[j]装满容量为j的背包有多少种方法
int[] dp = new int[num + 1];
dp[0] = 1;//初始化
for (int i = 0; i < nums.length; i++) {
for (int j = num; j >= 0; j--) {
if (j - nums[i] >= 0)
dp[j] += dp[j - nums[i]];
}
}
return dp[num];
}
}
题目介绍
给你一个二进制字符串数组 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 。
个人思路
这道题问的是子集最多可以放几个元素,其中有两个限制条件。这与之前做的题目多了一个条件,这里背包容量确定上就有所区别,我们可以考虑用二维dp数组来确定1个背包2个分区容量。注意到这里每个元素只能放一次,所以这是一道01背包的变式。
动规五部曲
确定dp数组及其下标含义
dp[i][j]
表示能容纳i个0,j个1的背包最多能容纳几个物品
确定递推公式
dp[j][k] = Integer.max(dp[j][k], dp[j - num_0][k - num_1] + 1);
解释一下:本质还是放与不放的问题。
dp[j - num_0][k - num_1] + 1
dp[i][j]
就是还没放入物品 i 的情况所以我们保留两者更大的就是此时遍历到物品 i 可放入的最大数量
初始化dp数组
默认初始化全为0即可,无需再次初始化,相当于隐式初始化
确定遍历顺序
三重for循环:最外层遍历物品,里面两层遍历背包的不同分区容量
下列所说的左上角元素:dp[j - num_0][k - num_1] + 1
打印dp数组检验
其实本题和此前做过的背包装最大价值和最多石头差不多,只不过本题是最大数量,区别在于前面加的是价值或石头重量/价值,本题加的是数量;另一个升级点,本题两个条件作为背包容量限制条件