[leetcode刷题] 动态规划中的背包问题

本文详细梳理了01背包和完全背包问题的动态规划解法,包括最值问题、存在性和组合问题。通过实例分析,如分割等和子集、目标和、石头游戏、一和零、零钱兑换等,阐述了背包问题的转换、递推公式、初始化和解题步骤。特别强调了完全背包中排列问题的处理和初始化的重要性。
摘要由CSDN通过智能技术生成

第一次复习时间:09-27
第三次复习时间:很久没刷题了 2022-07-23

参考链接

  • 微信参考1

  • 微信参考2

  • 参考1

  • 参考2

  • 参考3

  • 写给自己的废话

    • 简单的说二维bp也算是比较好理解的,dp递推方程就是max(使用第i中物品 和 不使用第i种物品)理解 i表示0-i的物品可以选择 j表示背包大小
    • 01背包初始化为了节省空间利用的滚动数组的思想,简单的说:把二维看成网格,其实每一次数据利用的都是正上面的数据和正上面左边的某个数据,所以完全可以缩减成一层:相当于只是利用同一层当前位置的数据和左边的某一个数据
    • dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
    • dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    • 注意看上面的两种写法

结论

  • 常见的背包我们就分成两种

    • 01背包:即要求数组的元素不可以重复使用,外层循环num数组(物品)内层循环遍历target并且倒序
    • 完全背包:即数组元素可以重复使用,正常情况下,外层循环num数组(物品)内层循环遍历target并且正序+if判断 j>=nums[i](就是要求背包大于物品的大小)。但是遇到组合问题中的排列即需要考虑元素间的顺序,此时要求target在外,nums数组在内颠倒
  • 背包都有三种问题分类0927补充 :只是模板实际i要换成j来写

    • 1、最值问题: dp[i] = max/min(dp[i], dp[i-nums]+1)或dp[i] = max/min(dp[i], dp[i-num]+nums);
    • 2、存在问题(bool):dp[i]=dp[i]||dp[i-num];
    • 3、组合or排列问题:dp[i]+=dp[i-num];特别注意是排列还是组合
  • 解题步骤

    • 判断是不是背包问题,其实这个其实并不难。需要存在数组存在target,从数组中选择元素满足这个target就是了。当前了需要注意的是有时候不会直接给你nums数组,或者不会直接给你target,你需要自己把抽象的问题具体化
    • 确定nums和target
    • 判断是01背包还是完全背包 这样就可以确定target正序还是逆序。
    • 确定是3种问题的哪一种,最值、存在还是组合(排列),这样就可以确定nums在外在内。(补充:可以再确定最值的数量还是价值)
    • 特别关注初始化 (vector dp() 和 dp【0】,比如常见易错:最值dp【0】=0 组合dp【0】=1)(补充:最小值的初始化)
    • 例子 322题
  • 最后看看别人的总结

补充:区别回溯和背包例如39:组合总和 如果需要有一个数组 一个target 需要返回所有组合的可能的方案 (不是方案书 不是存在 最值 那这个题就只能回溯 暴力所有的情况)
补充:看到数组 最值 是否 方案数 看看能不能转换为背包
补充:最值中的数量问题+1 价值问题+num[i]
补充: 特别注意最小值问题的初始化(完全背包的第一题)
补充:完全背包 内部正序 j>=coins[i] 记得等于 容易忘记

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

01 背包题目

416 分割等和子集(中等)(0 1 背包)(存在问题)

在这里插入图片描述

  • 1、最值问题: dp[i] = max/min(dp[i], dp[i-nums]+1)或dp[i] = max/min(dp[i], dp[i-num]+nums);

  • 2、存在问题(bool):dp[i]=dp[i]||dp[i-num];

  • 3、组合问题:dp[i]+=dp[i-num];
    -for(int j = bagWeight; j >= weight[i]; j--)注意在一维初始化是包含在了递归方程中如下示例代码

  • 再遇到一个nums 一个 目标和的题目就要想到背包问题。从结果上理解,如果背包最大化是 放入 dp【10】2 3 5 那是不是也可以理解成dp【10】 取决于dp【5】 dp【5 】取决于dp【2】 有点像回溯的改进 。

class Solution {
public:
 bool canPartition(vector<int> &nums)
{
    int sum = accumulate(nums.begin(), nums.end(), 0);
    if(sum % 2 ==1) return false;
    int target =sum/2;
    vector<int> dp(target+1,0);//下标表示背包和 最大=target
    dp[0]=true; //初始化:target=0不需要选择任何元素,所以是可以实现的
    for(int i =0;i<nums.size();i++)//遍历数组
    {
        for(int j =target;j>=nums[i];j--)//逆序遍历背包
        {
            //不选择物品i dp[i-1][j] 选择物品i dp[i-1][j-num[i]]
            dp[j]=dp[j]||dp[j-nums[i]];//表示上一层
        }
    }
    return dp[target];
}

};
  • 第一次复习

    • 题目转变成从数组中挑选得到 sum/2(target) 稍微转换一下
    • 0 1 背包 外层nums 内层 target 存在问题 dp[j]=dp[j]|dp[j-nums[i]]表示把当前的放入背包 和 不放入背包。
    • 写错一: 内层for循环 j>=nums[i]背包要大于物品的大小 才考虑放不放
    • 写错二:初始化为target+1
  • 第三遍复习:因为很久没做过了 很多都忘记了 补充一下对于地推公式的理解,就是你看二维的 在i个物品j的大小 取决于 i-1个商品的选择 + 第i个商品到底选择不选择(确定不选择呢 不选择那就是i-1 j的大小不变 。 如果选择 那就是 i-1 j-一个值 也合理)。简单的说就是把从i个商品中的选择 割裂成i-1的商品选择 和 确定第i个商品选择 和 不选择两种情况

  • 写错的地方就是>=

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        // Num 有了 target 就是目标的总数的一半 存在问题 而且不是排列
        int sum = accumulate(nums.begin(),nums.end(),0);
        if(sum %2==1) return false;
        int target =sum/2;
        vector<int> dp(target+1,0);//下标表示背包和 最大是targte 所以这边是target+1
        dp[0]=true;
        for(int i=0;i<nums.size();i++)
        {
            for(int j=target;j>=nums[i];j--)//注意这边的判断 剩余的背包数 要大于元素的大小
            {
                dp[j]=dp[j]||dp[j-nums[i]];
            }

        }
        return dp[target];
    }
};

494目标和(中等)(01背包 组合问题)(特别)

在这里插入图片描述

  • 1、最值问题: dp[i] = max/min(dp[i], dp[i-nums]+1)或dp[i] = max/min(dp[i], dp[i-num]+nums);

  • 2、存在问题(bool):dp[i]=dp[i]||dp[i-num];

  • 3、组合问题:dp[i]+=dp[i-num];

  • 属于第三种情况,

  • 问题的targe为正数和(主要是这边需要转换)

  • 从nums数组中挑选数组成正数和,并且每个数字的位置是固定的,不存在顺序所以是组合

  • 初始化 :一开始方式数都为0 只有dp【0】=1

  • 一样的 递推方程两种状态,选择当前数 不选择当前数 ,dp[]表示方案数 不选择当前的方案数 选择当前的方案数

class Solution {
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 == 1) return 0; // 此时没有方案
        if (nums.size() == 1 && sum < abs(S)) return 0;//这个是一个特判 看评论
        int bagSize = (S + sum) / 2;
        vector<int> dp(bagSize + 1, 0);
        dp[0] = 1;
        for (int i = 0; i < nums.size(); i++) {
            for (int j = bagSize; j >= nums[i]; j--) 
            {
                // 不选择 选择当前物品 
                dp[j] =dp[j]+dp[j - nums[i]];
            }
        }
        return dp[bagSize];
    }
};
  • 第一遍复习

    • 如果不是做过不是很确定能不能想到是背包问题,(我们需要转换题目 :转换为数组中选择 == target)这个很容易
    • x -y =target; x+y=sum; x = (sum+target)/2
    • 01 背包 逆序 外层nums 内层 target
    • 代码没啥问题 就是有一些细节处理 还有一个需要处理的看到数组 最值 是否 方案数 看看能不能转换为背包
  • 第三遍复习:重新写了一遍 前面的推导就没有去弄了

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(target>sum) return 0;
        if((target+sum)%2==1) return 0;
       if (nums.size() == 1 && sum < abs(target)) return 0;//这个是一个特判 看评论
       int bagSize= (target+sum)/2;
       vector<int> dp(bagSize+1,0);
       dp[0]=1;
       for(int i=0;i<nums.size();i++)
       {
           for(int j=bagSize;j>=nums[i];j--)
           {
               dp[j]=dp[j]+dp[j-nums[i]];
           }
       }
        return dp[bagSize];
    }
};

1049最后一块石头的重量2(中等)(01 背包 最值问题)(重要 需要转换)(注意递推方程)

在这里插入图片描述

在这里插入图片描述

  • 完全没想到是背包问题,这个题最难的是转换成背包问题来解决,并且这个题是间接求解
  • 最值问题: dp[i] = max/min(dp[i], dp[i-nums]+1)或dp[i] = max/min(dp[i], dp[i-num]+nums);
这道题看出是背包问题比较有难度
最后一块石头的重量:从一堆石头中,每次拿两块重量分别为x,y的石头,若x=y,则两块石头均粉碎;若x<y,两块石头变为一块重量为y-x的石头求最后剩下石头的最小重量(若没有剩下返回0)
问题转化为:把一堆石头分成两堆,求两堆石头重量差最小值
进一步分析:要让差值小,两堆石头的重量都要接近sum/2;我们假设两堆分别为A,B,A<sum/2,B>sum/2,若A更接近sum/2,B也相应更接近sum/2
进一步转化:将一堆stone放进最大容量为sum/2的背包,求放进去的石头的最大重量MaxWeight,最终答案即为sum-2*MaxWeight;0/1背包最值问题:外循环stones,内循环target=sum/2倒叙,应用转移方程1
class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) 
    {
        //问题转变为最大重量 最大值题
        //dp[j]表示当前背包可以放入的最大重量
        int sum = accumulate(stones.begin(), stones.end(), 0);
        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],dp[j-stones[i]]+stones[i]);
            }
        }
        return sum-2*dp[target];
    }
};
  • 第一遍复习
    • 第二次做很容易想到,01背包 dp【j】表示 大小为j的背包所能放入的最大重量
    • 有一 个需要注意的是 max最值公式有两个 dp[i] = max/min(dp[i], dp[i-nums]+1)或dp[i] = max/min(dp[i], dp[i-num]+nums);

474 一和零(中等)(01背包最值问题)(特别双维度(两个target))

在这里插入图片描述

  • 第一眼看确实没想到是背包问题,但是仔细看还是可以看出是背包问题,有数组 有target
  • 这个题特别指出在于有两个target需要同时满足。总结就是1个变量需要2维,2个变量则需要3维
  • 分析步骤
    • dp[i][j] 下标表示最多i个0 和 j个1 背包的大小
    • 确定递推公式,最值问题正常的是dp[j]=max(dp[j],dp[j-nums[i]+1]) 本题的是:dp[i][j] = max(dp[i][j], dp[i - zero][j - one] + 1); 我们需要统计一下每个元素字符串中0的个数 和1的个数
    • 初始化:老样子 vector都是0 特别是dp【0】【0】=0 此时子集个数只能是0 区别于组合的初始化
    • 确定递归顺序:01背包 非组合问题 外层 数组 两个内层分别是 背包大小i和j谁先谁后都是可以的逆序
class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) 
    {
        vector<vector<int>> dp(m+1,vector<int>(n+1,0));//初始化为0
        for(auto str:strs)
        {
            int zero=0;//分别统计每一个物品0和1的个数 每次清0
            int one=0;
            //首先统计一下每一个物品中0和1的个数方便后面循环
            for(auto c :str)
            {
                if(c=='0') zero++;
                else one++;
            }
            for(int i =m;i>=zero;i--)
            {
                for(int j=n;j>=one;j--)
                {
                    dp[i][j]=max(dp[i][j],dp[i-zero][j-one]+1);
                }
            }
           
        }
         return dp[m][n];

    }
};
  • 第一遍复习

    • 没啥问题 比较特别的背包问题罢了
    • 最值中的数量问题 +1
  • 第三遍复习:其实看一下 就很像背包问题 只是要做一些处理而已 没做过还真不清楚 看一下之前的代码 很容易理解,主要不好想。

完全背包

322 零钱兑换(中等)(完全背包)(最值问题)(特别注意这个题的初始化)

数组内挑选元素 = target的

  • 1、最值问题: dp[i] = max/min(dp[i], dp[i-nums]+1)或dp[i] = max/min(dp[i], dp[i-num]+nums);
  • 2、存在问题(bool):dp[i]=dp[i]||dp[i-num];
  • 3、组合问题:dp[i]+=dp[i-num];
    在这里插入图片描述
  • 最值问题
  • 背包target = 11 数组为coins
  • 这个题为完全背包 外层为物品 内层为正序背包 + 一个判断
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) 
    {
        //这个s
        //第二个是为了最后的判断 n没有一个符合的结果就是max 好判断
        
        vector<long long> dp(amount+1,INT_MAX);
        dp[0]=0;//价格为0需要0个硬币
        for(int i =0;i<coins.size();i++)//外层遍历数组 内层遍历背包
        {
            for(int j=0;j<=amount;j++)
            {
                if(coins[i]<=j)//如果背包够大 不够大就不变
                //不选当前硬币 选择当前硬币 数量+1,如果是截至就是加value(i)
                {
                     dp[j]=min(dp[j],dp[j-coins[i]]+1);
                }
        
            }
        }
        if(dp[amount]==INT_MAX) return -1;
        
        return dp[amount];
    }
};
  • 第一遍复习
    • 看代码中我的分析过程
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) 
    {
        // 1 是背包问题
        // 2 完全背包 正序+判断
        // 3 最小值 数量 nums 在外
        // 4 初始化 特殊
        vector<long long> dp(amount+1,INT_MAX);
        dp[0]=0;
        for(int i=0;i<coins.size();i++)
        {
            for(int j=0;j<=amount;j++)
            {
                if(j>=coins[i])
                {
                    dp[j]=min(dp[j],dp[j-coins[i]]+1);
                }
            }
        }
         if(dp[amount]==INT_MAX) return -1;//这题的特殊处理
          return dp[amount];
        

    }
};
  • 第三遍复习 还有就是有一些特殊处理 和初始化 看前面的代码注释就可以了

279 完全平方数(中等)(完全背包最值)(特别 推出nums)

在这里插入图片描述

  • 不难看出背包问题,唯一的区别在于物品(数组没有给你)你要自己推出来。int i=0;i<=sqrt(n);i++ 背包的元素是i*i 这边的i不再是数组的下标。
  • dp表示的是最小的数量,同样为了初始化,vector<long long> dp(n+1,INT_MAX); dp[0]=0
class Solution {
public:
    int numSquares(int n) 
    {
        //dp肯定表示最小值 初始化就要为INT_MAX
        vector<long long> dp(n+1,INT_MAX);
        dp[0]=0;
        //外层物品
        for(int i=0;i<=sqrt(n);i++)
        {
            //内层背包 完全背包正序 加判断
            for(int j=0;j<=n;j++)
            {
                //如果背包够放当前这个物品
                if(j>=i*i)
                {
                    //不选 选择
                    dp[j]=min(dp[j],dp[j-i*i]+1);
                }
            }
        }
        return dp[n];

    }
};
  • 第二遍复习 没啥问题
class Solution {
public:
    int numSquares(int n) {
        vector<long long> dp(n+1,INT_MAX);
        dp[0]=0;
        // 外层 num 内存 target 正序
        for(int i=0;i<=sqrt(n);i++)
        {
            for(int j=0;j<=n;j++)
            {
                if(j>=i*i)
                {
                    dp[j]=min(dp[j],dp[j-i*i]+1);
                }
            }
        }
        return dp[n];

    }
};
  • 第三遍复习 这个还是比较简单的 没什么问题

377 组合问题4(中等)(完全背包 排列问题)(特别)

在这里插入图片描述

  • 有目标 和 nums 标准的完全背包问题,返回的是方案数

  • 1、最值问题: dp[i] = max/min(dp[i], dp[i-nums]+1)或dp[i] = max/min(dp[i], dp[i-num]+nums);

  • 2、存在问题(bool):dp[i]=dp[i]||dp[i-num];

  • 3、组合or排列问题:dp[i]=dp[i]+dp[i-num];

  • 状态三:不选择当前元素,选择当前元素

  • 这是完全背包中的方案数(排列问题)所以需要外层target 内层nums 外层正序

  • 特别之处在于这个题测试样例有的通过不了 是超过了 INT_MAX 所以做了一个特判 并且 把int 换成 longlong

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) 
    {
        int n =nums.size();
        vector<long long> dp(target+1,0);//dp表示方案数 初始都为0 如果完全不匹配 返回0
        dp[0]=1;//和为0 只有一种可能
        for(int j=1;j<=target;j++)
        {
            for(int i=0;i<n;i++)
            {
                if(j >= nums[i] && (dp[j]+dp[j-nums[i]] <INT_MAX) )//注意等于
                {
                    dp[j]=dp[j]+dp[j-nums[i]];//不选择 选择
                }
            }
        }
      return dp[target]; 
    }
};


  • 第二遍复习

    • 没啥问题
    • 确定 num 和 target
    • 确定是完全背包 正序+判断
    • 确定是方案数排列 nums在内 target在外
    • 初始化 dp【0】=1
  • 第三遍复习

    • 我有点疑惑这边的初始值为什么是1,如果不确定其实可以自己举个例子测试一下 比如target=1 [1] .

518零钱兑换2(中等)(完全背包的组合问题)

在这里插入图片描述

  • 如果求组合数就是外层for循环遍历物品,内层for遍历背包。

  • 如果求排列数就是外层for遍历背包,内层for循环遍历物品。

  • 没什么特别的就是从例子中区分这个题是组合非排序

class Solution {
public:
int change(int amount, vector<int> &coins)
{
    vector<int> dp(amount + 1);
    dp[0] = 1;
    for (int coin : coins)
        for (int i = 1; i <= amount; i++)
            if (i >= coin)
                dp[i] += dp[i - coin];
    return dp[amount];
}

};
  • 第三遍复习,没什么问题,一遍过

70 爬楼梯进阶版(附加)(完全背包的 排列问题)

  • 题目:改为:每次可以爬 1 、 2或者m 个台阶。问有多少种不同的方法可以爬到楼顶呢?
  • 1阶,2阶,m阶就是物品,楼顶就是背包。
    应该发现这就是一个完全背包问题了!所以需将target放在外循环,将nums放在内循环。
class Solution {
public:
    int climbStairs(int n) {
        vector<int> dp(n + 1, 0);
        dp[0] = 1;
        for (int i = 1; i <= n; i++) { // 遍历背包
            for (int j = 1; j <= m; j++) { // 遍历物品
                if (i - j >= 0) dp[i] += dp[i - j];
            }
        }
        return dp[n];
    }
};
  • 第三遍 没啥问题
class Solution {
public:
    int climbStairs(int n) {
        vector<int> dp(n+1,0);
        dp[0]=1;
        //完全背包中的排列问题
        for(int j=0;j<=n;j++)//遍历背包
        {
            for(int i=1;i<=2;i++)//遍历物品
            {
                if(j>=i)
                {
                    dp[j]=dp[j]+dp[j-i];
                }
            }
        }
        return dp[n];

    }
};

139 单词拆分(中等)(完全背包中的排列)(特别重要!)

在这里插入图片描述

  • 转化为是否可以用 wordDict 中的词组合成 s,完全背包问题,并且为“考虑排列顺序的完全背包问题”,
  • 所以外层循环是target,内层循环是单词,target是正序+if的判断条件 这次是j要大于添加单词的长度
  • 重要如下
  • 我对源代码进行修改 把 if的判断移到了下面其实更好理解 就是 dp[j]取决于不选择这个单词 和 选择了这个单词,选择这个单词并不是那么简单的还需要进行一个判断这个单词和背包字符串末尾是否匹配的上。所以变成如下,切记比对是背包后面的单词比对这样才能推到更小的背包考虑
  • dp[i] = dp[i] || s.substr(i - size, size) == word && dp[i - size];
  • 正常的元素之和 就好像一段绳子 不需要比对 元素长度的绳子放在任意位置都是匹配的
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        vector<bool> dp(s.size() + 1);
        dp[0] = true;
        for(int i = 1; i <= s.size(); i++){
            for(auto& word: wordDict){
                int size = word.size();        
                if (i - size >= 0 )
                    dp[i] = dp[i] || s.substr(i - size, size) == word && dp[i - size];            
            }       
        }
        return dp[s.size()];
    }   
};


  • 第一遍复习
    • 没啥问题 之前写的很清楚了,把i换成j 符合书写习惯
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) 
    {
        vector<bool> dp(s.size()+1);
        dp[0]=true;//不选就可以组成
        //完全背包 正序 排序 外层是target
        for(int j=0;j<=s.size();j++)
        {
            for(auto word:wordDict)
            {
                int size =word.size();
                if(j>=size)//表示可以放入
                {
                    dp[j]=dp[j]||dp[j-size] && s.substr(j-size,size)==word;;//不放入 放入
                } 
            }
        }
        return dp[s.size()];

    }
};
  • 第三遍 没什么问题 其实j从0或者1开始都是一样的
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        //很明显是完全背包的排序问题 下标表示长度
        vector<bool> dp(s.size()+1);
        dp[0]=true;//下标其实就是 长度为0是可以的 

        for(int j=1;j<=s.size();j++)//外层是target
        {
            for(auto word:wordDict)
            {
                int size=word.size();
                if(j>=size)//表示可以放在
                {
                    dp[j]=dp[j]||dp[j-size] && s.substr(j-size,size)==word;;//不放入 放入
                }
            }
        }
        return dp[s.size()];


    }
};

总结

  • 易错的地方
    • 初始化容易写错。
    • 返回值有时候还需要进行判断,就是初始值不变那就是没有结果。
    • 完全背包 内层循环 j从1开始。
    • 区别排列问题和组合问题的顺序。
    • 抽象问题转换成具体问题
    • 理解本质就很好写

如有错误,欢迎指出,排版不是很好,主要以自己的记录复习为主。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Windalove

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值