您现在的位置是:首页 >技术交流 >Day43【动态规划】1049.最后一块石头的重量 II、494.目标和、474.一和零网站首页技术交流

Day43【动态规划】1049.最后一块石头的重量 II、494.目标和、474.一和零

林沐华 2024-06-17 11:26:43
简介Day43【动态规划】1049.最后一块石头的重量 II、494.目标和、474.一和零

1049.最后一块石头的重量 II

力扣题目链接/文章讲解

视频讲解

还是需要转化为 0-1 背包问题:物品装入背包,求装入的最大价值(每个物品至多装入一次)

要把01背包问题套到本题上来,需要确定

  • 背包容量
  • 物品价值
  • 物品重量

转化的核心思路:尽量让石头分成重量相同的两堆,则这两堆相撞之后剩下的石头最小

我们希望尽可能凑成重量为 sum / 2 的石头堆,假如我们将石头看成一定重量的物品,问题转化成想将这些具有一定重量的物品装入容量为 sum / 2 的背包,看看最大能装多少重量

我们只需要设置物品重量和价值相等,则能装多少重量  = 装入的最大价值 

这样问题就转过来了: 

  • 背包容量:sum / 2
  • 物品价值:石头重量
  • 物品重量:石头重量

剩下的套 0-1 背包问题滚动数组的解题思路就好 

代码如下 

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int sum = accumulate(stones.begin(), stones.end(), 0);
        int bagSize = sum / 2;
        // 背包容量bagSize
        // 物品重量stones
        // 物品价值stones
        vector<int> dp(bagSize + 1);    // 滚动数组,下标代表背包容量,值代表装入的最大价值
        // 初始化原二维dp数组的第一行,即将物品0装入容量为j的背包
        for (int j = 0; j <= bagSize; ++j) {
            if (j >= stones[0]) // 背包容量大于等于物品0的重量
                dp[j] = stones[0];  // 物品0的价值
            else    // 背包装不下物品0
                dp[j] = 0;
        }
        for (int i = 1; i < stones.size(); ++i) {   // 从第二行(i=1)开始遍历填充原二维dp数组
            for (int j = bagSize; j >= 0; --j) {    // 倒序填充原二维dp数组每一行的元素
                if (j >= stones[i]) // 容量j能装下物品。注意右边的dp表示的是i-1层的dp
                    dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
                else    // 装不下。注意右边的dp表示的是i-1层的dp
                    dp[j] = dp[j];
            }
        }
        return (sum - dp[bagSize]) - dp[bagSize];   // 第一个括号里的值为另一堆石头的重量,dp[bagSize]为这一堆石头的重量
    }
};

注意上述代码在 for 循环中控制条件可以进行一定程度的优化,但是我们给出最直观的代码逻辑 

494.目标和

力扣题目链接/文章讲解

视频讲解

本题可以用回溯,但是大概率超时

还是用 0-1 背包的思路

如何转化为 0-1 背包问题呢

假设加法的总和为 bagSize,那么减法对应的总和就是 sum - bagSize

所以我们要求的是 bagSize - (sum - bagSize) = target

bagSize = (target + sum) / 2

此时问题就转化为,装满容量为 bagSize 的背包,有几种方法

好像问题有变化,变成了装满背包的方法数,没办法按照之前物品装包求最大价值的思维模式了

好吧,那就还是从动态规划五部曲开始,由二维 dp 数组推导吧

1、确定 dp 数组下标及值的含义 

dp[i][j]:下标 i, j 表示从下标为 [0 - i] 的物品中任意取装满容量为 j 的背包,dp[i][j] 的值表示从物品下标 [0 - i] 任取物品装满容量为 j 的背包有多少种方法

2、确定递推公式 

dp[i][j] 的值表示从下标为 [0 - i] 的物品中任取,装满容量为 j 的背包的方法数,怎么求这个 dp[i][j] 呢?为了求 dp[i][j],肯定需要考虑从下标为 0 到 i 的物品中取物然后装满容量为 j 的背包的装入方案。得到这个 dp[i][j] 的方案有两种:一种是不放物品 i 装满了背包,另一种是放了物品 i 装满了背包

  • 不放物品 i:已经确定里面不放物品 i 的装入方法数,为物品 0 到 i - 1 装满容量为 j 的背包的方法数,即 dp[i - 1][j]
  • 放物品 i:已经确定要放物品 i 时的装入方法数,为物品 0 到 i - 1 装满容量为 j - nums[i] 的背包的方法数,即 dp[i - 1][j - nums[i]]

两种方案各自方法数加起来就是总的装满容量为 j 的背包的方法数,即递推公式:dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]] 

注意这个递推公式的前提条件是背包要能装下物品 i,否则只需要考虑不放物品 i 的方案 

3、dp 数组初始化

由递推公式,我们首先观察到 dp[i][j] 的值都是由上一行(层)的值推出来的(即由 i - 1 那一行推出来),故一定要初始化第一行,即 dp[0][j] 需要被初始化

怎么初始化 dp[0][j] 呢?考虑 dp 数组下标及值的含义:取物品 0(从下标为 0 到 0 的物品中取)装满容量为 j 的背包的方法数 

  • 当物品0重量非0,则当背包容量为0时有一种装满方案(啥也不装),当背包容量为物品0的重量时有一种装满方案(就把物品0装进去),其他背包容量都无法装满
  • 当物品0重量为0,则当背包容量为0时候有一种装满方案(啥也不装或装入物品0),其他背包容量都无法装满

第二行及之后的位置随意初始化,反正都会被遍历覆盖

4、确定遍历顺序 

从上层(上一行)遍历填充到下层(下一行)就可以(因为当前层的值是由上一层推出来的,需要保证上一层是已经更新后的正确的值),单层中从左到右或者从右到左遍历填充均可 

代码中,外层 for 循环遍历物品即可(一个物品的索引代表一行,一行一行遍历) 

5、打印dp数组验证

代码如下

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if ((target + sum) % 2 == 1) return 0; // 此时没有方案
        if (abs(target) > sum) return 0; // 此时没有方案
        int bagSize = (target + sum) / 2;   // 加法的总和(背包容量)
        
        // 背包容量:bagSize
        // 物品重量:nums
        // 求装满背包有多少种方法
        
        //1、确定 dp 数组下标及值的含义:下标为从物品0 - i选择取装满容量为j的背包,值为选择物品装满背包的方法数
        vector<vector<int> > dp(nums.size(), vector<int>(bagSize + 1));
        //2、确定递推公式:dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]]
        //3、dp数组初始化:由递推公式,我们首先观察到 dp[i][j] 的值都是由上一行(层)的值推出来的(即由 i - 1 那一行推出来),故一定要初始化第一行,即 dp[0][j] 需要被初始化 
        for (int j = 0; j <= bagSize; ++j) {
            if (nums[0] != 0) {
                if (j == 0 || j == nums[0]) // 当背包容量为0或背包容量为物品0的重量时,能用物品0装满
                    dp[0][j] = 1;
                else
                    dp[0][j] = 0;
            }
            else    // 物品重量为0
            {
                if (j ==  0)    // 背包容量为0时,可以不装,可以装物品0,两种方案
                    dp[0][j] = 2;
                else
                    dp[0][j] = 0;
            }
        }
        //4、遍历填充dp数组:一行一行填充
        for (int i = 1; i < nums.size(); ++i) { // 遍历物品(遍历填充每一行)
            for (int j = 0; j <= bagSize; ++j)  // 遍历背包容量(遍历填充每一行中的元素)
                if (j >= nums[i])   // 容量j能放下物品i的重量
                    dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];
                else
                    dp[i][j] = dp[i - 1][j];
        }
        //5、打印dp数组验证略
        return dp[nums.size() - 1][bagSize];
    }
};

本题同样,dp[i][j] 的值都是由上一行(层)的值推出来的,也可以利用滚动数组进行状态压缩,这里给出代码实现。递推公式左边的 dp 是第 i 层,右边的 dp 是第 i - 1 层

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if ((target + sum) % 2 == 1) return 0; // 此时没有方案
        if (abs(target) > sum) return 0; // 此时没有方案
        int bagSize = (target + sum) / 2;   // 加法的总和(背包容量)
        
        // 背包容量:bagSize
        // 物品重量:nums
        // 求装满背包有多少种方法
        
        //1、确定 dp 数组下标及值的含义:下标为装满容量为j的背包,值为装满背包的方法数
        vector<int> dp(bagSize + 1);
        //2、确定递推公式:dp[j] = dp[j] + [j - nums[i]]
        //3、dp数组初始化:一定要初始化原二维dp数组第一行
        for (int j = 0; j <= bagSize; ++j) {
            if (nums[0] != 0) {
                if (j == 0 || j == nums[0]) // 背包容量为0或物品0的重量,能用物品0装满
                    dp[j] = 1;
                else
                    dp[j] = 0;
            }
            else    // 物品重量为0
            {
                if (j ==  0)    // 可以不装,可以装物品0,两种方案
                    dp[j] = 2;
                else
                    dp[j] = 0;
            }
        }
        //4、遍历填充原二维dp数组:一行一行填充
        for (int i = 1; i < nums.size(); ++i) { // 遍历物品(遍历填充原二维数组每一行)
            for (int j = bagSize; j >= 0; --j)  // 遍历背包容量(遍历填充每一行中的元素),需要倒序遍历
                if (j >= nums[i])   // 容量j能放下物品i的重量
                    dp[j] = dp[j] + dp[j - nums[i]];
                else
                    dp[j] = dp[j];
        }
        //5、打印dp数组验证略
        return dp[bagSize];
    }
};

474.一和零 

力扣题目链接/文章讲解

视频讲解

本题转化为 0-1 背包问题:物品装入背包(一个物品最多装一次),求最大的装入物品数量

该题目容量/重量是两个维度

怎么理解容量/重量有两个维度?打个比方,一个背包的容量能装重量为 M 的固体和重量为 N 的液体,每个物品由重量为 m 的固体部分和重量为 n 的液体部分构成。若某个物品能装进背包,需要物品的固体部分总重量和液体部分总重量均分别小于等于背包对于固体的容量及对于液体的容量,才能放进 

但是本质上该题还是 0-1 背包,因为一个物体只能被选一次 

来,动态规划三部曲!

1、确定 dp 数组下标及值的含义 

dp[i][j][k]:下标 i, j, k 表示从下标为 [0 - i] 的物品中任意取装入有两个容量维度的背包,这两个容量维度分别为 j 和 k,dp[i][j][k] 的值表示从物品下标 [0 - i] 任取物品装进背包最多能装入多少个物品

2、确定递推公式  

为了求 dp[i][j][k],肯定需要考虑从下标为 0 到 i 的物品中取物然后装进有两个容量维度的背包的装入方案。得到这个 dp[i][j][k] 的方案有两种:一种是不装物品 i 进背包,另一种是装物品 i 进背包

  • 不放物品 i:已经确定里面不放物品 i,则最大装入物品数量是从物品 0 到 i-1 中选择物品装入容量维度为 j k 的背包的最大装入物品数量,即 dp[i - 1][j][k]
  • 放物品 i:已经确定要放物品 i ,则最大装入物品数量是从物品 0 到 i-1 中选择物品装入容量维度为 j - 物品 i 所占的该维度容量k - 物品 i 所占的该维度容量的背包的最大装入物品数量加一,即 dp[i - 1][j - weight0[i]][k - weight1[i]] + 1

两种方案的最大值就是 dp[i][j][k],即:dp[i][j][k] = max(dp[i-1][j-weight0[i]][k-weight1[i]] + 1, dp[i-1][j][k])

注意这个递推公式的前提条件是背包要能装下物品 i,否则只需要考虑不放物品 i 的方案  

3、dp 数组初始化

由递推公式,我们首先观察到 dp[i][j][k] 的值都是由 i - 1 那个维度的 dp 值推出来的,故 dp[0][j][k] 需要被初始化

怎么初始化也很简单,结合 dp 数组下标含义:若物品 0 能够放进容量维度分别为 j 和 k 的背包,则最多能装入一个物品,dp[0][j][k] 为1;若装不下物品 0, 则一个物品也装不进去,dp[0][j][k] 为 0

4、确定遍历顺序 

从上层遍历填充到下层就可以(因为当前层 i 的值是由上一层 i - 1 的值推出来的,需要保证上一层是已经更新后的正确的值),单层中任意顺序遍历填充均可 

代码中,外层 for 循环遍历物品即可

5、打印 dp 数组验证

代码如下

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        vector<int> weight0, weight1;
        for (auto & str : strs) {
            int num0 = 0;
            int num1 = 0;
            for (auto c : str)
            {
                if (c == '0')
                    ++num0;
                if (c == '1')
                    ++num1;
            }
            weight0.push_back(num0);
            weight1.push_back(num1);
        }   // 统计每个物品的两个重量维度
        // 背包两个容量维度分别为 m 和 n

        // 定义dp数组
        vector<vector<vector<int> > > dp(strs.size(), vector<vector<int> >(m + 1, vector<int>(n + 1)));
        // 初始化dp数组
        for (int j = 0; j <= m; ++j) {
            for (int k = 0; k <= n; ++k) {
                if (j >= weight0[0] && k >= weight1[0]) // 若物品0能放进背包
                    dp[0][j][k] = 1;
                else    // 从物品0中任选,一个物品也放不进
                    dp[0][j][k] = 0;
            }
        }
        // 遍历。从i为1开始遍历填充
        for (int i = 1; i < strs.size(); ++i) {
            for (int j = 0; j <= m; ++j)
                for (int k = 0; k <= n; ++k) {
                    if (j >= weight0[i] && k >= weight1[i]) {   // 若物品i能放进背包
                        dp[i][j][k] = max(dp[i-1][j-weight0[i]][k-weight1[i]] + 1, dp[i-1][j][k]);
                    }
                    else    // 物品i不能放进背包
                        dp[i][j][k] = dp[i-1][j][k];
                }
        }
        return dp[strs.size() - 1][m][n];
    }
};

同样,本题也可以状态压缩

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        vector<int> weight0, weight1;
        for (auto & str : strs) {
            int num0 = 0;
            int num1 = 0;
            for (auto c : str)
            {
                if (c == '0')
                    ++num0;
                if (c == '1')
                    ++num1;
            }
            weight0.push_back(num0);
            weight1.push_back(num1);
        }   // 统计每个物品的两个重量维度
        // 背包两个容量维度分别为 m 和 n

        // 定义dp数组
        vector<vector<int> > dp(m + 1, vector<int>(n + 1));
        // 初始化dp数组
        for (int j = 0; j <= m; ++j) {
            for (int k = 0; k <= n; ++k) {
                if (j >= weight0[0] && k >= weight1[0]) // 若物品0能放进背包
                    dp[j][k] = 1;
                else    // 从物品0中任选,一个物品也放不进
                    dp[j][k] = 0;
            }
        }
        // 遍历。从i为1开始遍历填充
        for (int i = 1; i < strs.size(); ++i) {
            for (int j = m; j >= 0; --j)
                for (int k = n; k >= 0; --k) {  // 需要倒序遍历!这里是二维的,即需要从下往上,从右往左遍历填充
                    if (j >= weight0[i] && k >= weight1[i]) {   // 若物品i能放进背包
                        dp[j][k] = max(dp[j-weight0[i]][k-weight1[i]] + 1, dp[j][k]);
                    }
                    else    // 物品i不能放进背包
                        dp[j][k] = dp[j][k];
                }
        }
        return dp[m][n];
    }
};

为什么需要倒序遍历? 

根据非滚动数组版本的递推公式,我们更新当前层 i 层某位置元素依赖的是上一层 i - 1 层的元素,更详细地说是上一层对应当前位置及对应当前位置左上角的元素(注意这里每层是二维数组),对应到滚动数组中,更新某位置元素依赖的是当前位置元素的旧值及当前位置左上角的元素的旧值(这里的旧值指的是上一轮外层 for 循环所填充的值)。只有倒序遍历从下向上,从右向左填充,才能保证当前位置及当前位置左上角的元素保留上一轮外层循环的旧值。如果正序遍历,则当前位置的左上角元素的值可能在本轮外层循环中已经被更新了,无法持有上一轮循环更新得到的值了


回顾总结 

我们用0-1背包解决了哪些问题? 

物品装入背包的最大价值

物品能否装满背包

背包最多能装多重的物品

物品装满背包有多少种方法

装满最多有多少个物品 

时刻记住滚动 dp 数组与二维 dp 数组是如何对应的!滚动数组递推公式右边是 i - 1 层,左边是 i 层

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。