一、LeetCode 1049.最后一块石头的重量II
文章讲解/视频讲解:https://programmercarl.com/1049.%E6%9C%80%E5%90%8E%E4%B8%80%E5%9D%97%E7%9F%B3%E5%A4%B4%E7%9A%84%E9%87%8D%E9%87%8FII.html#%E6%80%9D%E8%B7%AF
状态:已解决
1.思路
其实这个题跟上个题416. 分割等和子集 - 力扣(LeetCode)很像,我们可以把整个石头分成两堆,为了使碰撞后剩余石头重量最小,那么就尽量使两堆石头的重量接近,怎么才能使两堆石头重量尽量接近呢?努力凑齐某堆石头接近sum/2即可。那么这道题就转换成了昨天的416题。具体分析看LeetCode 416.分割等和子集-CSDN博客。
这题最后要求最小的石头重量,那么我们装sum/2的背包时,得到的价值(重量)dp[n]是 <= sum/2。故剩下的那堆石头价值(重量)>= dp[n],也就是sum-dp[n] >= dp[n]。故碰撞后剩余石头重量为 (sum-dp[n]) - dp[n]。
2.代码实现
class Solution {
public:int lastStoneWeightII(vector<int>& stones) {int sum = 0;for(int i=0;i<stones.size();i++){sum += stones[i];}int m = stones.size(),n = sum/2;vector<int> dp(n+1,0);vector<bool> flag(m,false);for(int i=0;i<m;i++){for(int j=n;j>=stones[i];j--){dp[j] = max(dp[j],dp[j-stones[i]]+stones[i]);}}return (sum-dp[n])-dp[n];}
};
二、494. 目标和
文章讲解/视频讲解:https://programmercarl.com/0494.%E7%9B%AE%E6%A0%87%E5%92%8C.html
状态:已解决
1.思路
这道题其实是有点难想的,最直接的思路就是回溯。那采用动规又该如何解决呢?我们注意看,我们向数组中的每个整数前添加 '+'
或 '-'
,然后串联起所有整数后,得到的实则为一堆数减去另一堆数的表达式。我们将所有的被减数作为一个整体,设为left,将所有的减数作为一个整体,设为right。由于此题的数组nums依旧是一开始都给定的,且要求每个数都要参与计算,那么left 和 right 的和就是固定的sum(nums中所有元素的和)。即:
解得:
由于 sum 和 target 都是样例给定的固定值,因此left也是固定值。故只需找到原数组有多少子集和为left,就可得到满足条件的表达式个数。那么此题就变成了一个背包问题:背包容量为left,物品数为nums.size(),重量为nums[i]。
(1)确定dp数组以及下标的含义:
dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法。
(2)确定递推公式:
有哪些来源可以推出dp[j]呢?假如遍历到nums[i]时,那么凑成dp[j]就要看dp[j - nums[i]] 有多少种方法。因为nums有多个,那么每次遍历一个nums[i]都有一个对应相等的 j,故总的dp[j]就为所有j - nums[i] 的和。即dp[j] += dp[j - nums[i]]。
例如:dp[j],j 为5,
- 已经有一个1(nums[i]) 的话,有 dp[4]种方法 凑成 容量为5的背包。
- 已经有一个2(nums[i]) 的话,有 dp[3]种方法 凑成 容量为5的背包。
- 已经有一个3(nums[i]) 的话,有 dp[2]中方法 凑成 容量为5的背包
- 已经有一个4(nums[i]) 的话,有 dp[1]中方法 凑成 容量为5的背包
- 已经有一个5 (nums[i])的话,有 dp[0]中方法 凑成 容量为5的背包
那么凑整dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。所以求组合类问题的公式,都是类似这种。
(3)dp数组初始化:
如果数组[0] ,target = 0,那么 bagSize = (target + sum) / 2 = 0。 dp[0]也应该是1, 也就是说给数组里的元素 0 前面无论放加法还是减法,都是 1 种方法。所以本题我们应该初始化 dp[0] 为 1。
(4)确定遍历顺序:
在一维背包-CSDN博客中,我们讲过对于01背包问题一维dp的遍历,nums放在外循环,target在内循环,且内循环倒序。
(5)举例推导dp数组:
输入:nums: [1, 1, 1, 1, 1], S: 3;bagSize = (S + sum) / 2 = (3 + 5) / 2 = 4
dp数组状态变化如下:
2.代码实现
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 m = nums.size();int n = (sum + target)/2;vector<int> dp(m+1,0);if(abs(sum)<target) return 0;if((sum+target)%2==1) return 0;dp[0] = 1;for(int i=0;i<nums.size();i++){for(int j=n;j>=nums[i];j--){dp[j] += dp[j-nums[i]];}}return dp[n];}
};
时间复杂度:O(n × m),n为正数个数,m为背包容量
空间复杂度:O(m),m为背包容量
三、474.一和零
文章讲解/视频讲解:https://programmercarl.com/0474.%E4%B8%80%E5%92%8C%E9%9B%B6.html
状态:已解决
1.思路
这道题其实就是在01背包问题的基础上,将背包的维度上升了。也就是,决定背包容量的属性不仅只有重量一栏了,现有0的数量和1的数量两栏,二者共同决定了背包容量。那么这道题就转换成了一个背包问题:容量由m和n决定,物品有strs.size()个,每个物品的重量也是两个维度(0的个数和1的个数),每个物品的价值为1,现求该背包最多能装多少物品。
(1)确定dp数组以及下标:
dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]。
(2)确定递推公式:
跟一维类似,dp[i][j]由没添加strs[i]的左上角状态而来,即dp[i][j] = dp[i - zeroNum][j - oneNum] + 1(因为strs[i]加入了子集,故子集大小+1) 。由于可能部分集合strs[i] 0和1的个数相同,故dp[i][j]需要一直维护最大的dp[i - zeroNum][j - oneNum] + 1。
(3)dp数组的初始化:
跟前面的题没差,01背包的dp数组(滚动数组)初始化为0就可以。因为物品价值不会是负数,初始为0,保证递推的时候dp[i][j]不会被初始值覆盖。
(4) 确定遍历顺序:
还是跟前面的题一样,最外层物品正序,内层(两层)都是倒序。
(5) 举例推导dp数组:
以输入:["10","0001","111001","1","0"],m = 3,n = 3为例
最后dp数组的状态如下所示:
2.代码实现
class Solution {
public:int findMaxForm(vector<string>& strs, int m, int n) {vector<vector<int>> dp(m+1,vector<int>(n+1,0));for(string str:strs){int zeroNum = 0,oneNum = 0;for(int i=0;i<str.size();i++){if(str[i] == '0') zeroNum++;else oneNum++;}for(int i = m;i >= zeroNum; i--){for(int j = n; j >= oneNum; j--){dp[i][j] = max(dp[i-zeroNum][j-oneNum]+1,dp[i][j]);}}}return dp[m][n];}
};
时间复杂度: O(kmn),k 为strs的长度
空间复杂度: O(mn)