Bootstrap

动态规划part04

文章参考来源代码随想录

题目参考来源1049. 最后一块石头的重量 II - 力扣(LeetCode)494. 目标和 - 力扣(LeetCode)

474. 一和零 - 力扣(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.一和零

转化思路:

其实本题并不是多重背包,再来看一下这个图,捋清几种背包的关系

416.分割等和子集1

多重背包是每个物品,数量不同的情况。

本题中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数组的状态如下所示:

474.一和零

代码: 

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扩空间

;