文章参考来源代码随想录
题目参考来源1049. 最后一块石头的重量 II - 力扣(LeetCode)494. 目标和 - 力扣(LeetCode)
1049.最后一块石头的重量II
转化思路:
把石头分成尽可能重量接近的两堆,然后那这两堆相减就是最小重量了;
且每颗石头仅能使用一次,抽象为01背包
动规五部曲:
1.确定dp数组以及下标i的含义:
这里01背包是:容量为i(这里说容量更形象,其实就是重量)的背包,最多能装入价值为dp[i]的物品
本题石头重量等于石头价值,因此这里的含义是:容量为i的背包,最多能装入重量为dp[i]的石头
2.确定递推公式:
01背包:dp[i]=max(dp[i],dp[i-weigh[i]]+value[i])
本题:石头重量等于石头价值,dp[i]=max(dp[i],dp[i]-stone[i]]+stone[i])
3.dp数组如何初始化:
解题思路里说了,这里要把石头分成两堆
而且下标i表示背包容量,题目中最大容量:
因为提示中给出1 <= stones.length <= 30,1 <= stones[i] <= 1000,所以最大重量就是30 * 1000
那么背包应该分配15000;
或者这里求出背包石头总重量,然后分配sum/2;
全部初始化为0即可,因为后面递推会覆盖掉的
4.遍历顺序:
与之前写01背包一维的时候一样,先物品再背包,外层0到stone.size,内层背包容量到目前物品的重量
5.举例推导dp:
举例,输入:[2,4,1,1],此时target = (2 + 4 + 1 + 1)/2 = 4 ,dp数组状态图如下:
需要注意的点:
最后dp[target]里是容量为target的背包所能背的最大重量。
注意背包最大能装的重量dp[target]不一定就等于sum/2,如stone[2,7,4,1,8,1]等于11
那么分成两堆石头,一堆石头的总重量是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) {
vector<int>dp(15001,0);
int sum=accumulate(stones.begin(),stones.end(),0);
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];
}
};
- 时间复杂度:O(m × n) , m是石头总重量(准确的说是总重量的一半),n为石头块数
- 空间复杂度:O(m)
494. 目标和
回溯:
本题要如何使表达式结果为target,
既然为target,那么就一定有 left组合 - right组合 = target。
left + right = sum,而sum是固定的。right = sum - left
left - (sum - left) = target 推导出 left = (target + sum)/2 。
target是固定的,sum是固定的,left就可以求出来。
此时问题就是在集合nums中找出和为left的组合。(本题用回溯会超时间)
代码1:
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
if (sum == target) {
result.push_back(path);
}
// 如果 sum + candidates[i] > target 就终止遍历
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, sum, i + 1);
sum -= candidates[i];
path.pop_back();
}
}
public:
int findTargetSumWays(vector<int>& nums, int S) {
int sum = 0;
for (int i = 0; i < nums.size(); i++) sum += nums[i];
if (S > sum) return 0; // 此时没有方案
if ((S + sum) % 2) return 0; // 此时没有方案,两个int相加的时候要格外小心数值溢出的问题
int bagSize = (S + sum) / 2; // 转变为组合总和问题,bagsize就是要求的和
// 以下为回溯法代码
result.clear();
path.clear();
sort(nums.begin(), nums.end()); // 需要排序
backtracking(nums, bagSize, 0, 0);
return result.size();
}
};
动态规划(二维dp数组):
转化思路:
这里把加法集合(x)和减法集合(sum-x)分开成两堆
可以知道x+(sum-x)=sum
x-(sum-x)=target解出来x=(target+sum)/2
此时问题就转化为,用nums装满容量为x的背包,有几种方法。
此时会有疑问这个x不会出现向下取的情况吗:这时这种情况就不能出现了
所以
if ((target + sum) % 2 == 1) return 0; // 此时没有方案
以及
if (abs(target) > sum) return 0; // 此时没有方案
因为每个物品(题目中的1)只用一次!
这次和之前遇到的背包问题不一样了,之前都是求容量为j的背包,最多能装多少。
本题则是装满有几种方法。其实这就是一个组合问题了。
动规五部曲:
1.确定dp数组以及下标i的含义:
用下标从0到i的num[i]去填满容量为j的背包一共有dp[i][j]种方法
2.确定递推公式:
先只考虑物品0,如图:
(这里的所有物品,都是题目中的数字1)。
装满背包容量为0 的方法个数是1,即 放0件物品。
装满背包容量为1 的方法个数是1,即 放物品0。
装满背包容量为2 的方法个数是0,目前没有办法能装满容量为2的背包。
接下来 考虑 物品0 和 物品1,如图:
装满背包容量为0 的方法个数是1,即 放0件物品。
装满背包容量为1 的方法个数是2,即 放物品0 或者 放物品1。
装满背包容量为2 的方法个数是1,即 放物品0 和 放物品1。
其他容量都不能装满,所以方法是0。
接下来 考虑 物品0 、物品1 和 物品2 ,如图:
装满背包容量为0 的方法个数是1,即 放0件物品。
装满背包容量为1 的方法个数是3,即 放物品0 或者 放物品1 或者 放物品2。
装满背包容量为2 的方法个数是3,即 放物品0 和 放物品1、放物品0 和 物品2、放物品1 和 物品2。
装满背包容量为3的方法个数是1,即 放物品0 和 物品1 和 物品2。
从以上我们可以讨论dp[2][2]:
不放物品2有dp[1][2]个方法
放物品2应该有dp[1][2-1]个方法(物品2的重量为nums[2])
抽象为:dp[i][j]=dp[i-1][j]+dp[i-1][j-nums[i]]
要保证j>nums[i]需要再加一个判断语句
3.dp数组如何初始化:
第一列:容量为0的背包有1种方法(全都不放),所以dp[i][0]=1
第一行:dp[0][nums[0]]刚好装满,初始化为1;
拓展:
如果有两个物品,物品0为0, 物品1为0,装满背包容量为0的方法有几种。
- 放0件物品
- 放物品0
- 放物品1
- 放物品0 和 物品1
此时是有4种方法。
其实就是算数组里有t个0,然后按照组合数量求,即 2^t 。
所以此时第一列的初始化应该为:
int numZero = 0;
for (int i = 0; i < nums.size(); i++) {
if (nums[i] == 0) numZero++;
dp[i][0] = (int) pow(2.0, numZero);
}
4.遍历顺序:
递推公式可以知道dp[i][j]依赖他上方和左上方的元素
所以这里从左往右,从上到下
因为是二维dp,所以先背包先物品都可
5.举例推导dp:
输入:nums: [1, 1, 1, 1, 1], target: 3
bagSize = (target + sum) / 2 = (3 + 5) / 2 = 4
dp数组状态变化如下:
代码2:
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum=accumulate(nums.begin(),nums.end(),0);
if((sum+target)%2)return 0;
if(abs(target)>sum)return 0;
int bagsize=(sum+target)/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];
}
};
- 时间复杂度:O(n × m),n为正数个数,m为背包容量
- 空间复杂度:O(m),m为背包容量
474.一和零
转化思路:
其实本题并不是多重背包,再来看一下这个图,捋清几种背包的关系
多重背包是每个物品,数量不同的情况。
本题中strs 数组里的元素就是物品,每个物品都是一个!
而m 和 n相当于是一个背包,两个重量维度的背包。
动规五部曲:
1.确定dp数组以及下标i的含义:
i个0和j个1的字符串数组最大长度为dp[i][j]
2.确定递推公式:
01背包:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
这里实际上是有两个重量维度,即字符串中的zeronum(0的个数)和onenum(1的个数)
而dp[i][j]本身就相当于价值了
价值/长度是取当前背包1(0)的个数-当前字符串1(0)的个数的位置所代表的最大长度(即dp[i-zeronum][j-onenum])再加上1
这里还是要和原先dp[i][j]比较,所以递推公式:dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);(形式上是二维,但是本质是滚动数组)
3.dp数组如何初始化:
第一行和第一列都无意义,具体数据会在遍历字符串的时候具体统计的
所以一律初始化为0
4.遍历顺序:
本质上是滚动数组,所以先物品再背包
在本题中,物品就是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);
}
}
5.举例推导dp:
以输入:["10","0001","111001","1","0"],m = 3,n = 3为例
最后dp数组的状态如下所示:
代码:
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(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];
}
};
需要注意的点:
在初始化dp时,与重量维度相关的数要再往上+1扩空间