• LeetCode刷题笔记【33】:动态规划专题-5(最后一块石头的重量 II、目标和、一和零)


    前置知识

    今天是动态规划专题的第5篇, 也是背包问题的第2篇.
    所以本文和动态规划专题的1~3弱相关, 和上一篇, 也就是动态规划-4强相关.

    相比于昨天的经典背包问题的思路与模板, 今天侧重于如何将其他问题理解为背包问题, 并且如何对具体的情景进行调整.
    并且今天的三道题都是广义的"01背包问题", 即"物品只有选择/不选择两种情况".

    参考文章:
    LeetCode刷题笔记【29】:动态规划专题-1(斐波那契数、爬楼梯、使用最小花费爬楼梯)
    LeetCode刷题笔记【30】:动态规划专题-2(不同路径、不同路径 II)
    LeetCode刷题笔记【31】:动态规划专题-3(整数拆分、不同的二叉搜索树)
    LeetCode刷题笔记【32】:动态规划专题-4(二维背包问题、一维背包问题、分割等和子集)

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

    题目描述

    在这里插入图片描述

    LeetCode链接:https://leetcode.cn/problems/last-stone-weight-ii/description/

    解题思路

    参考昨天最后一题<416. 分割等和子集>, 昨天是要我们选出两组数, 然后让两组数的和相同;
    今天是让我们选出一对儿一对儿的石头, 互相碰, 让最后剩下的石头最小; (看似差别较大)

    但其实可以转化为:“选出两组石头, 让两组石头互相碰, 从而让剩下的结果最小

    这样一来, 就可以使用和<416. 分割等和子集>一样的思路, 先求sum, 然后将sum/2作为bagSize(target), 使用背包问题的过程, 从而求得dp[target]的值, 也就是"面对target这么大的背包, 我们最多能装多少石头".

    唯一需要注意的是, 最后的结果应该是 sum-2*dp[target], 因为是求剩下了多少石头嘛.

    代码

    class Solution {
    public:
        int lastStoneWeightII(vector<int>& stones) {
            int sum=0;
            for(int stone : stones)
                sum += stone;
            int target = sum/2;
            vector<int> dp(target+1);
            for(int i=0; i<stones.size(); ++i){
                for(int j=target; j>=stones[i]; --j){
                    dp[j] = max(dp[j], stones[i] + dp[j-stones[i]]);
                }
            }
            return sum - dp[target] - dp[target];
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    494. 目标和

    题目描述

    截图

    LeetCode链接:https://leetcode.cn/problems/target-sum/description/

    用回溯算法

    ① 回溯遍历穷举(虽然时间复杂度非常高, 但是高低是通过了)

    class Solution {
    private:
        int ans=0;
        int curSum=0;
        void backtrack(vector<int>& nums, int target, int index){
            if(index>=nums.size()){
                if(curSum==target)
                    ans++;
                return;
            }
            curSum += nums[index];
            backtrack(nums, target, index+1);
            curSum -= nums[index]*2;
            backtrack(nums, target, index+1);
            curSum += nums[index];
            return;
        }
    public:
        int findTargetSumWays(vector<int>& nums, int target) {
            backtrack(nums, target, 0);
            return ans;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    转换为背包问题动态规划

    ② 转换为背包问题, 动态规划
    既然最后可以得到target, 那么一定可以将所有数分为leftright两组, 有如下关系:
    left+right=sum, left-right=terget
    推导得到: left = (sum+target)/2, 这样一来, 问题就转化为"nums中有多少种数字组合, 可以让和为(sum+target)/2"

    class Solution {
    public:
        int findTargetSumWays(vector<int>& nums, int target) {
            int sum=0;
            for(int num : nums)
                sum += num;
    
            if(abs(target)>sum)
                return 0;
            if((sum+target)%2==1)
                return 0;
    
            int left = (sum+target)/2;
            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){
                    dp[j] += dp[j-nums[i]];
                }
            }
            return dp[left];
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    这还有一个问题, 就是关于递推公式为什么是dp[j] += dp[j-nums[i]];
    其实写成 dp[j] = dp[j] + dp[j-nums[i]] 更方便理解, 解释起来就是:
    ① 现在的背包容量是j
    ② 一方面之前还没有物品i的时候, 我有dp[i]种将容量为j的背包装满的方法(上一行中的内容)
    ③ 现在有了物品i, 其重量是nums[i], 那么此时的装满背包的方法, 一方面要考虑原有的dp[i], 还要考虑装入物品i后, 装满剩余空间的方法数量(dp[j-nums[i]])

    所以面对容量为j的背包, 和0~i种物品, 有dp[j] = dp[j] + dp[j-nums[i]]种装包方法

    在这里插入图片描述
    如果将一维dp数组展开成二维, 展现其更新推导过程, 则如上图所示

    还有一个点是关于初始化, 为什么要让dp[0]=1, 我的理解是对于容量为0的背包, 你手上一个物品都没有(0个物品), 那么你就只有1种方法: 啥都不装. 所以为1.

    474.一和零

    题目描述

    截图

    LeetCode链接:xxx(记得加点击跳转链接)

    解题思路

    <代>: 其实还是01背包问题, 但此时背包中有两个bagSize的维度
    递推公式是: dp[i][j] = max(dp[i][j], dp[i-zeroNum][i-oneNum]+1)
    其中zeroNumoneNum当前0和1的数量

    代码

    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(int 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];
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    总结

    简单01背包问题分割等和子串最后一块石头的重量目标和
    问什么问题面对大小为bagSize的背包和n件物品, 怎么装收益最大面对大小为sum/2的背包, 用nums中的num作为物品, 能不能将背包装满面对sum/2的背包, 我用这些石头尽量多装, 剩下的空间最少是多少面对大小为(sum+target)/2的背包, 我用nums中的num作为物品来装满它, 有几种装法?
    背包大小bagSizetarget=sum/2target=sum/2left=(sum+target)/2
    是否要装满不一定需要, 否则返回false不一定, 求最少剩下多少空间一定
    返回结果最大收益dp.back() / dp.(bagSize)装满了(dp[target]==target, 则true)/没装满(dp[target]!=target, 则false)sum-2*dp[target]dp[left]
    dp数组的含义背包大小j, 有0~i物品, 最大收益同左, 但weightvalue都用nums代替同左左, 但stones同时担任weightvalue的作用背包大小j, 有0~i物品, 装满背包的方法数量
    递推公式dp[j]=max(dp[j], value[i]+dp[j-weight[i]])dp[j]=max(dp[j], nums[i]+dp[j-nums[i]])dp[j]=max(dp[j], stones[i]+dp[j-stones[i]])dp[j] = dp[j]+dp[j-nums[i]]
    trick二维换一维, 偷空间复杂度sum%2==1了直接return false和左右两边需要检测奇数偶数不同, 虽然这里也有target=sum/2, 但可以直接用int性质向下取整sum%2==1 或者 (sum+target)%2==1了直接return false
    备注遇到其他问题想不出来了, 就往这个经典问题上靠, 甚至别强求一维背包, 画一下二维背包帮助理解类似于右value数组和weight数组意义重合, 二者的功能被stones数组同时担任可以看出和前三个几乎不是一卦的, 要注意区分, 以及理解递推公式

    本文参考:
    最后一块石头的重量 II
    目标和
    一和零

  • 相关阅读:
    3.3 栈的表示和操作的实现
    【算法】常见位运算总结
    Log4j2远程代码执行漏洞靶场复现(CVE-2021-44228)
    文件或目录损坏且无法读取
    Android逆向工程【黑客帝国】
    智引未来:2024年科技革新引领工业界变革与机遇
    基于Java的大学生救援队网站
    Ubuntu中增加交换内存
    【POJ No. 3067】 公路交叉数 Japan
    vins-mono初始化代码分析
  • 原文地址:https://blog.csdn.net/Eibosinu/article/details/132812437