1049.最后一块石头的重量II
文档讲解:代码随想录 (programmercarl.com)
视频讲解:动态规划之背包问题,这个背包最多能装多少?LeetCode:1049.最后一块石头的重量II_哔哩哔哩_bilibili
状态:没想到。
思路
本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了。
两堆重量不一定相等,比如总重量为X,
如果分开后两堆重量正好都为X/2,则为0;
如果分开后两堆重量为Y和Z(假设Y<Z),需要令Y尽可能大,才能在(Y+Z)固定的情况下,使得(Z-Y)尽可能小。由于Y<Z且Y+Z=X,所以Y小于等于X/2。为了令Y尽可能大,需要借助【01背包问题解法】求在背包容量为X/2时,收到的物品价值Y(各个石头重量和)尽可能大。
本题物品的重量为stones[i],物品的价值也为stones[i]。对应着01背包里的物品重量weight[i]和 物品价值value[i]。
动规五步曲:
-
确定dp数组以及下标的含义
dp[j]表示容量为j的背包,最多可以背最大重量为dp[j]。
01背包中,dp[j]的含义,容量为j的背包,最多可以装的价值为 dp[j]。
相对于 01背包,本题中,石头的重量是 stones[i],石头的价值也是 stones[i] ,可以 “最多可以装的价值为 dp[j]” == “最多可以背的重量为dp[j]”
-
确定递推公式
01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
本题则是:dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
-
dp数组如何初始化
既然 dp[j]中的j表示容量,那么最大容量(重量)是多少呢,就是所有石头的重量和。
而我们要求的target其实只是最大重量的一半,所以计算出石头总重量 然后除2,得到dp数组的大小。
接下来就是如何初始化dp[j]呢,因为重量都不会是负数,所以dp[j]都初始化为0就可以了,这样在递归公式dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);中dp[j]才不会初始值所覆盖。
-
确定遍历顺序
如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!
-
举例推导dp数组
举例,输入:[2,4,1,1],此时target = (2 + 4 + 1 + 1)/2 = 4 ,dp数组状态图如下:
最后dp[target]里是容量为target的背包所能背的最大重量。
那么分成两堆石头,一堆石头的总重量是dp[target],另一堆就是sum - dp[target]。
在计算target的时候,target = sum / 2 因为是向下取整,所以sum - dp[target] 一定是大于等于dp[target]的。
那么相撞之后剩下的最小石头重量就是 (sum - dp[target]) - dp[target]。
代码
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
int sum = 0;
for(int i = 0; i < stones.size(); i++) sum += stones[i];
int bagweight = sum / 2;
vector<int> dp(bagweight + 1, 0);
for(int i = 0; i < stones.size(); i++){
for(int j = bagweight; j >= stones[i]; j--){
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
return (sum - dp[bagweight] - dp[bagweight]);
}
};
494.目标和
文档讲解:代码随想录 (programmercarl.com)
视频讲解:动态规划之背包问题,装满背包有多少种方法?| LeetCode:494.目标和_哔哩哔哩_bilibili
状态:做不出来。下面写的详解比“文档讲解”和“视频讲解”好。
又一个新类型题:前面的dp[j]表示容量为j的背包能装的物品最大价值是多少,本题的dp[j]表示填满容量为j的背包有几种方法。
思路
因为 left组合 - right组合 = target、left + right = sum,target是固定的,sum是固定的,
所以
left = (target + sum) / 2
。此时问题就转化为,装满容量为(target+sum)/2 的背包,有几种方法。
小细节:
- 若(target+sum)%2==1,则无解,具体原因参照视频讲解中的举例;
- 若abs(target)>sum,也无解。
下面是“视频讲解”下方评论的解法,比up主的清晰,即01背包的过程
首先是二维数组解法
可以把状态定义为
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-1】[j-nums【i】];
- 取负号,那么前i-1个元素的和为j+nums【i】,那么前i-1个元素就要组成和为j+nums【i】的方案数dp【i-1】[j+nums【i】]。
所以两种选择的方案数相加就是dp【i】【j】。
二维数组解法不需要应用left = (target + sum) / 2
公式进行转换。
但是这样定义状态会导致空间复杂度过高,因为我们需要一个二维数组来存储所有可能的状态。
所以应用上面讲的left = (target + sum) / 2
公式转换为一维的01背包问题。也就是说,我们只需要找到有多少种方法可以从数组中选出若干个元素使得它们的和等于(target + sum(nums)) / 2即可。这就变成了一个经典的01背包问题。
可以把状态定义为dp【j】,表示用数组中若干个元素组成和为j的方案数。那么状态转移方程就是:
dp【j】 = dp【j】 + dp[j - nums【i】]
这个方程的意思是,如果我们要用若干个元素组成和为j的方案数,那么有两种选择:不选第i个元素或者选第i个元素。
如果不选第i个元素,那么原来的元素和为j,那么原来已经有多少种方案数就不变,即dp【j】;
如果选第i个元素,那么剩下的元素和为j - nums【i】,要组成和为j - nums【i】 的方案数就等于dp[j - nums【i】]。
所以两种选择相加就是等号左边的dp【j】。
但是在实现这个状态转移方程时,有一个细节需要注意:由于每次更新dp【j】都依赖于之前计算过得dp值(也就是说当前行依赖于上一行),所以我们必须从后往前遍历更新dp值(也就是说从右往左更新),否则会覆盖掉之前需要用到得值。
最后返回dp【bag_size】即可。
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
for(int i = 0; i < nums.size(); i++) sum += nums[i]; // 计算数组中所有元素的和
if((target + sum) % 2 == 1) return 0; // 无解情况1
if(abs(target) > sum) return 0; //无解情况2
int bagWeight = (target + sum) / 2; //计算背包容量,也就是要组成的和
vector<int> dp(bagWeight + 1, 0); //初始化动态规划数组,长度为背包容量加一,初始值都为零
dp[0] = 1; //表示用若干个元素组成和为零的方案数,只有一种就是什么都不选
for(int i = 0; i < nums.size(); i++){ // 遍历数组中每个元素
for(int j = bagWeight; j >= nums[i]; j--){ // 倒序遍历背包容量从大到小,直到小于当前元素值停止
dp[j] = dp[j] + dp[j - nums[i]]; // 更新dp【j】,表示选或者不选当前元素的方案数之和
}
}
return dp[bagWeight]; //和为bagWeight的情况有几种
}
};
474.一和零
文档讲解:代码随想录 (programmercarl.com)
视频讲解:动态规划之背包问题,装满这个背包最多用多少个物品?| LeetCode:474.一和零_哔哩哔哩_bilibili
状态:不会做。这是“背包的有两个维度”的题目。
思路
本题中strs 数组里的元素就是物品,不同长度的字符串就是不同大小的待装物品,每个物品都是一个!
而m 和 n相当于是一个背包,两个维度的背包。
动规五部曲
-
确定dp数组(dp table)以及下标的含义
dp[i][j]
:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]
。 -
确定递推公式
dp[i][j]
可以由前一个strs里的元素(字符串)推导出来,假设字符串t有zeroNum个0,oneNum个1。dp[i][j]
=dp[i - zeroNum][j - oneNum]
+ 1。dp[i - zeroNum][j - oneNum]
表示不包含字符串t的最大子集大小。在遍历的过程中,取
dp[i][j]
的最大值。所以递推公式:dp[i][j]
= max(dp[i][j]
,dp[i - zeroNum][j - oneNum]
+ 1);
此时大家可以回想一下01背包的递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
对比就会发现,字符串的zeroNum和oneNum相当于物品的重量(weight[i]),字符串本身的个数相当于物品的价值(value[i])。
这就是一个典型的01背包! 只不过物品的重量有了两个维度而已。
-
dp数组如何初始化
01背包的dp数组初始化为0就可以。因为物品价值不会是负数,初始为0,保证递推的时候dp[i][j]不会被初始值覆盖。
-
确定遍历顺序
外层for循环遍历物品,内层for循环遍历背包容量且从后向前遍历!
物品就是strs里的字符串,背包容量就是题目描述中的m和n。
for (string str : strs) { // 遍历物品 int oneNum = 0, zeroNum = 0; for (char c : str) { if (c == '0') zeroNum++; else oneNum++; } for (int i = m; i >= zeroNum; i--) { // 遍历背包容量且从后向前遍历! for (int j = n; j >= oneNum; j--) { dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1); } } }
遍历背包容量的两层for循环先后循序有没有什么讲究?
没讲究,都是物品重量的一个维度,先遍历哪个都行!
代码
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
vector<vector<int>> dp(m + 1, vector<int> (n + 1, 0)); // 默认初始化0
for(string str: strs){ // 遍历物品
int zeroNum = 0, oneNum = 0;
for(char c: str){
if(c == '0') ++zeroNum;
else ++oneNum;
}
for(int i = m; i >= zeroNum; i--){ // 遍历背包容量且从后向前遍历!
for(int j = n; j >= oneNum; j--){
dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
}
}
}
return dp[m][n];
}
};