代码随想录算法训练营
---day36
文章目录
- 代码随想录算法训练营
- 前言
- [一、1049. 最后一块石头的重量 II](#一、1049. 最后一块石头的重量 II)
- 二、494.目标和
- [三、474. 一和零](#三、474. 一和零)
- 总结
前言
今天是算法营的第36天,希望自己能够坚持下来!
今日任务:
● 1049. 最后一块石头的重量 II
● 494. 目标和
● 474.一和零
一、1049. 最后一块石头的重量 II
动态规划
思路:
其实是尽量让石头分成重量相同的两堆(尽可能相同),相撞之后剩下的石头就是最小的。
-
分割等和子集 是求背包是否正好装满,而本题是 求背包最多能装多少。
-
dp[i][j]的定义为:容量为j的背包,最多可以背最大重量为dp[j]
-
递归公式:dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
-
初始化:j表示容量,那么最大容量(重量)是多少呢,就是所有石头的重量和。因为提示中给出1 <= stones.length <= 30,1 <= stones[i] <= 1000,所以最大重量就是30 * 1000 。而我们要求的target是最大重量的一半,所以dp数组开到15000大小就可以了。也可以把石头遍历一遍,计算出石头总重量 然后除2,得到dp数组的大小。
-
遍历顺序:一维dp数组需要先物品,再遍历背包,且背包是倒序遍历的。
代码如下:
cpp
class Solution {
public:
//问题转化成用背包装接近总和一半的石头,与另一半相撞
//dp[j]就是容量为j时,背包最多能装的石头重量
//最后dp[target]里是容量为target的背包所能背的最大重量
int lastStoneWeightII(vector<int>& stones) {
int sum = 0;
for (int num:stones) {
sum += num;
}
int target = sum/2;
vector<int> dp(15001, 0); //题目说stones长度最大是30,stones[i]最大是100,取一半15000
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.目标和
一维dp数组
思路:
把问题转化成,需要将集合分成两组,一组正数,一组负数,两组相加得到目标和,作为正数集合和为left,作为负数集合和为right,得出left - right = target
而且left + right = sum, 可以得出 left = (sum+target)/2
- dp[j]:容量为j的背包,有dp[j]种方法可以装到left重量
- 递归公式:对于每一个num[i]都可以放,或者不放,
①如果放,那么dp[j]就相当于固定了num[i],方法的种数就决定于剩余容量(当前容量j减去num[i]占的容量)装满可以的种数,也就是固定了物品i后, dp[j] = dp[j-num[i]]
②那么总的dp[j]就等于所有物品i的dp[j]累加dp[j] += dp[j - nums[i]] - 初始化:如果数组中只有一个[0],target也是0,那么有1种方法 dp[0] = 1;
- 遍历顺序:一维dp数组需要先物品,再遍历背包,且背包是倒序遍历的。
代码如下:
cpp
class Solution {
public:
//本题可以理解成需要将集合分成两组,一组正数,一组负数,两组相加得到目标和
//left + right = sum
//left - right = target , right = left - target
//left + left - terget = sum -> left = (sum+target)/2 得出正数和可由sum和target得到
//再转化成在集合nums中找出和为left的组合
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
for (int num:nums) {
sum += num;
}
//当tartget大于总和无解,每个数只能用一次
if (abs(target) > sum) return 0;
//当求出来的left不为整数时说明无解
if ((sum + target)%2 == 1) return 0;
int left = (sum + target)/2;
//dp[j]含义:容量为j的背包,有dp[j]种方法可以装到left重量
//初始化:如果数组中只有一个[0],target也是0,那么有1种方法
//递推公式:对于每一个num[i]都可以放,或者不放,
//如果放,那么dp[j]就相当于固定了num[i],方法的种数就决定于剩余容量(当前容量j减去num[i]占的容量)装满可以的种数,也就是固定了物品i后, dp[j] = dp[j-num[i]]
//总的dp[j]就等于所有物品i的dp[j]累加dp[j] += dp[j - nums[i]]
vector<int> dp(left + 1, 0);
dp[0] = 1;
for (int i = 0; i < nums.size(); i++) {
for (int j = left; j >= nums[i]; j--) { //对于每个容量j,累加所有能放下的物品i的dp[j]
dp[j] += dp[j - nums[i]];
}
}
return dp[left];
}
};
二维dp数组
代码如下:
cpp
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 (abs(target) > sum) return 0; // 此时没有方案
if ((target + sum) % 2 == 1) return 0; // 此时没有方案
int bagSize = (target + sum) / 2;
vector<vector<int>> dp(nums.size(), vector<int>(bagSize + 1, 0));
// 初始化最上行
if (nums[0] <= bagSize) dp[0][nums[0]] = 1;
// 初始化最左列,最左列其他数值在递推公式中就完成了赋值
dp[0][0] = 1;
int numZero = 0;
for (int i = 0; i < nums.size(); i++) {
if (nums[i] == 0) numZero++;
dp[i][0] = (int) pow(2.0, numZero);
}
// 以下遍历顺序行列可以颠倒
for (int i = 1; i < nums.size(); i++) { // 行,遍历物品
for (int j = 0; j <= bagSize; j++) { // 列,遍历背包
if (nums[i] > j) dp[i][j] = dp[i - 1][j];
else dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];
}
}
return dp[nums.size() - 1][bagSize];
}
};
三、474. 一和零
思路:
strs 数组里的元素就是物品,每个物品都是一个,仍然是01背包问题,只是背包有两个。
- dp[i][j]:i个0和j个1的最大子集长度
- 递推公式: 对于当前字符串,可以选择加入子集或者不加入,
①加入就是先空出当前字符串所包含的0和1的个数,再+1,dp[i-zeroNum][j-oneNum]+1
②然后遍历的时候就取每个字符串所求出来的dp[i][j]的最大值
所以得出,dp[i][j] = max(dp[i][j], dp[i-zeroNum][j-oneNum] + 1) - 初始化:01背包的滚动数组的dp数组初始化为0就可以
- 遍历顺序:先遍历物品,再遍历背包(本题是有两个背包,m和n)
- 举例推导dp数组:
以输入:["10","0001","111001","1","0"],m = 3,n = 3为例,如下:
代码如下:
cpp
class Solution {
public:
//dp[i][j]:i个0和j个1的最大子集长度
//递推公式: 对于当前字符串,可以也是选择加入子集或者不加入,
//加入就是先空出当前字符串所包含的0和1的个数,再+1,dp[i-zeroNum][j-oneNum]+1
//然后遍历的时候就取每个字符串所求出来的dp[i][j]的最大值
//dp[i][j] = max(dp[i][j], dp[i-zeroNum][j-oneNum] + 1)
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;
//统计当前字符串的0和1的个数
for (char c : str) {
if (c == '0') zeroNum++;
else oneNum++;
}
for (int i = m; i >= zeroNum; i--) { //后序遍历背包容量,遍历0和1两个维度
for (int j = n; j>= oneNum; j--) {
dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
}
}
}
return dp[m][n];
}
};
总结
不同类型的背包问题,
- 分割等和子集:求是否刚好装满背包,
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]) 求dp[target] == target - 最后一块石头的重量 II:尽可能装满背包,
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]) 求sum - dp[target] - dp[target] - 目标和 :求装满背包的方法数,
dp[j] += dp[j- weight[j]] - 一和零:求装满背包用的最多物品数量(有两个背包),
dp[i][j] = max(dp[i][j], dp[i - 物品重量1][j - 物品重量2] + 1)
明天继续加油!