1049. 最后一块石头的重量 II
有一堆石头,用整数数组 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],这就是最优值。
思路
本题思路与昨日 416.分割等和子集 非常相似,每个石头的重量就等同于数组元素的值,因此同样适用weight[i]=value[i]
、stones[target]=target
,只不过本题明确是可以存在差值的(即最终剩下那块石头的余重),那么此时不等于sum/2
就不能直接判false
当我们找出一堆
dp[target]
的石头后,剩余石头重量就为(sum-dp[target])
, 由于sum/2
向下取整,因此必定有sum-dp[target] ≥ dp[target]
因此这两堆石头相撞后的剩余重量就为(sum-dp[target])-dp[target]
- dp数组及其下标含义:背包用容量
j
装了价值/重量d[j]
的石头 - 递推公式:同理为
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
- 数组初始化:同理受限于最大石头总重,可定义为
vector<int> dp(15001, 0);
- 遍历顺序:同理
i
从小到大,j
从大到小
代码实现
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
vector<int> dp(15001,0);
int sum=0;
for(int i=0; i<stones.size(); i++){
sum += stones[i];
}
int target = sum/2;
for(int i=0; i<stones.size(); i++){
for(int j=target; j>=stones[i]; j--){
dp[j] = max(dp[j], dp[j-stones[i]]+stones[i]);
}
}
return sum - dp[target] - dp[target];
}
};
494. 目标和
给你一个非负整数数组 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
思路
- 分解该问题,数组元素最终会分为正负两堆,则必有
正-负=target
- 此外数组元素的总和是已知且固定的,即
正+负=sum
- 联立上面两式消去负元素,可得
正=(target+sum)/2
- 此时问题就转化为了——只要在数组中找出一组和为
正
的元素,就表示构造出了一个运算结果为target
的表达式 - 建模为01背包问题:能装满背包容量为
正
的元素的所有方法
特别的是,本题不再求能不能装或装多少,而是装满有几种方法?这是一类特殊的组合问题。
- dp数组及其下标含义:填满容量为
j
的背包,有dp[j]
种方法 - 递推公式:
容量j==正
本质上还是一组数组元素和,因此它与nums[i]
息息相关:以nums=[1,2,3,4,5]
要填满dp[5]
为例,如果已有一个元素1,那么就只需要求dp[5-1]=dp[4]
即可,以此类推:- 遍历每个
nums[i]
,想要凑成容量j
都有dp[j-nums[i]]
种方法 - 因此总共有
dp += dp[j-num[i]]
种方法
- 遍历每个
- 数组初始化:
dp[0]=1
- 对于数组[0]的target=0,无论取正取负,元素0都只有一种方式成为target
- 遍历顺序:同理,外循环正序,内循环倒序
- 举例推导: 输入:nums: [1, 1, 1, 1, 1], target: 3,dp数组变化如图所示:
代码实现
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
for (int i = 0; i < nums.size(); i++) sum += nums[i];
int bagSize = (target + sum) / 2;
if (abs(target) > sum) return 0; // 超过总和,此时没有方案
if ((target + sum) % 2 == 1) return 0; // 奇数,此时没有方案
vector<int> dp(bagSize + 1, 0);
dp[0] = 1;
for (int i = 0; i < nums.size(); i++) {
for (int j = bagSize; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
}
return dp[bagSize];
}
};
474. 一和零
给你一个二进制字符串数组 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 。
思路
- dp数组及其下标含义:
- 递推公式:
- 数组初始化:
- 遍历顺序:
- 举例推导:
代码实现
后记
- 动态规划的基本问题,大多直接取dp数组的最终结果为输出,因此
dp[i]
的意义通常就是输出的意义,此时再代入去分析i
的含义即可 - dp[n]代表n的最终结果,但数组下标从0开始,因此dp[n]的实际存储位置为n+1,所以在定义数组时要考虑多定义一个长度。