【LeetCode】全面了解背包问题:所有分类+模板汇总

同步在:博客地址

0. 起源

0-1背包:最大最小问题

  • 概念:一共有N件物品,第i(i从1开始)件物品的重量为w[i],价值为v[i]。在总重量不超过背包承载上限W的情况下,能够装入背包的最大价值是多少?

  • 思路:定义一个二维数组dp 存储最大价值,其中dp[i][j]表示i件物品体积不超过j 的情况下能达到的最大价值。设第i件物品体积为w,价值为v,根据第i件物品是否添加到背包中,可以分两种情况讨论:

    • i件物品没添加到背包,总体积不超过j 的前i 件物品的最大价值就是总体积不超过j的前i-1件物品的最大价值, d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i-1][j] dp[i][j]=dp[i1][j]
    • i件物品添加到背包中 d p [ i ] [ j ] = d p [ i − 1 ] [ j − w [ i ] ] + v [ i ] dp[i][j] = dp[i-1][j-w[i]] + v[i] dp[i][j]=dp[i1][jw[i]]+v[i]
  • 状态转换方程: d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w [ i ] ] + v [ i ] ) , j > = w [ i ] dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]]+v[i]) , j >= w[i] dp[i][j]=max(dp[i1][j],dp[i1][jw[i]]+v[i]),j>=w[i]

  • 源代码:

    // w 表示背包总体积
    // weights 表示物体的重量
    // values 表示物体的价值,和 weights 一一对应
    int backpack(int w, vector<int> &weights, vector<int> &values) {
    	int n = weights.size();
    	vector<vector<int>> dp(n, vector<int>(w))
    	for(int i = 0; i < n; ++ i) {
    		for (int j = 0; j < w; ++ j) {
    			if (j >= w[i]) {
    				// 背包承受力足够放下第 i 个物体
    				dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]);
    			} else {
    				// 背包承受力不够放下第 i 个物体
    				dp[i][j] = dp[i-1][j];
    			}
    		}
    	}
    	return dp[n-1][w-1];
    }
    

最后:可以优化一下空间的,这里不细说了,注意第二个循环需要倒叙遍历,具体参考:CyC2018

完全背包问题

  • 概念:和0-1背包问题相似,只是每个重量的物体有无数多个,也就可以重复放某个价值的物体,最终目的就是让背包的总价值最大

  • 思路:总体思想和0-1背包一样

    • i件物品没添加到背包,同上, d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i-1][j] dp[i][j]=dp[i1][j]
    • i件物品添加到背包中,此时和01背包不太一样,因为每种物品有无限个(但注意书包限重是有限的),所以此时不应该转移到dp[i−1][j−w[i]]而应该转移到dp[i][j−w[i]],即装入第i种商品后还可以再继续装入第种商品。 d p [ i ] [ j ] = d p [ i ] [ j − w [ i ] ] + v [ i ] dp[i][j] = dp[i][j-w[i]] + v[i] dp[i][j]=dp[i][jw[i]]+v[i]
  • 状态转换方程: d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − w [ i ] ] + v [ i ] ) , j > = w [ i ] dp[i][j] = max(dp[i-1][j], dp[i][j-w[i]]+v[i]) , j >= w[i] dp[i][j]=max(dp[i1][j],dp[i][jw[i]]+v[i]),j>=w[i]

  • 源代码:

    // w 表示背包总体积
    // weights 表示物体的重量
    // values 表示物体的价值,和 weights 一一对应
    int backpack(int w, vector<int> &weights, vector<int> &values) {
    	int n = weights.size();
    	vector<vector<int>> dp(n, vector<int>(w))
    	for(int i = 0; i < n; ++ i) {
    		for (int j = 0; j < w; ++ j) {
    			if (j >= w[i]) {
    				// 背包承受力足够放下第 i 个物体
    				dp[i][j] = max(dp[i-1][j], dp[i][j-w[i]] + v[i]);
    			} else {
    				// 背包承受力不够放下第 i 个物体
    				dp[i][j] = dp[i-1][j];
    			}
    		}
    	}
    	return dp[n-1][w-1];
    }
    

同理可以优化空间,好像需要注意:第二个循环正序遍历,不太懂,具体参考完全背包-知乎

多重背包问题

  • 先不整理了,有点费脑子

混合背包或者多维度背包

1. LeetCode实战

参考:力扣-澜山

0. 组合问题

dp[i] += dp[i-num]
  
// 组合问题需考虑元素之间的顺序,需将target放在外循环,将nums放在内循环
for i in range(1, target+1):
    for num in nums:

377.组合总和 Ⅳ、494.目标和、518.零钱兑换 II

  • 这里展示一下377.组合总和 Ⅳ,这是需要考虑每个数字之间的顺序的,所以是组合问题

    • dp[x] 表示选取的元素之和等于 x的方案数,目标是求 dp[target]
    • 动态规划的边界是dp[0]=1,有当不选取任何元素时,元素之和才为 0,因此只有 1 种方案。
    • 1 < = i < = t a r g e t 1 <= i <= target 1<=i<=target,如果存在一种排列,其中的元素之和等于 i,则该排列的最后一个元素一定是数组 nums 中的一个元素,假设该排列的最后一个元素是 num,则一定有 n u m < = i num <= i num<=i,对于元素之和等于 i−num的每一种排列,在最后添加 num之后即可得到一个元素之和等于 i 的排列,因此在计算 **dp[i]**时,应该计算所有的 dp[i−num] 之和。
    • 参考:力扣官方-题解
    class Solution {
    public:
        int combinationSum4(vector<int>& nums, int target) {
            vector<unsigned long long> dp(target + 1);
            dp[0] = 1;
            for (int i = 1; i <= target; ++ i) {
                for (auto num: nums) {
                    if (i >= num ) {
                        dp[i] += dp[i-num];
                    }
                }
            }
            return dp[target];
        }
    };
    
    

    说明一下:这里使用unsigned long long是为了防止测试数据的溢出,其实这个测试用例有点内个,我的疑问解答

1. True、False问题公式

dp[i] = dp[i] or dp[i-num]

139.单词拆分、416.分割等和子集

  • 这里展示139.单词拆分dp[i] 表示 s 的前 i 位是否可以用 wordDict 中的单词表示

    class Solution {
    public:
        bool wordBreak(string s, vector<string>& wordDict) {
            vector<bool> dp(s.size() + 1);
            // 这里使用set容器是为方便查找
          	unordered_set<string> m(wordDict.begin(), wordDict.end());
            dp[0] = true;
            int maxL = 0;
          	// 这里做一个优化,每次j不需要从0开始遍历,wordDict里面有一个最小长度的串
            for (auto s: wordDict) {
                maxL = max(maxL, (int)s.length());
            }
            for (int i = 1; i <= s.size(); ++ i) {
                int start = max(i - maxL, 0);
                for (int j = start; j < i; ++ j) {
                    if (dp[j] && m.find(s.substr(j, i-j)) != m.end()) {
                        dp[i] = true;
                        break;
                    }
                }
            }
            return dp[s.size()];
        }
    };
    

2. 最大最小问题

dp[i] = min(dp[i], dp[i-num]+1) or dp[i] = max(dp[i], dp[i-num]+1)

474.一和零、322.零钱兑换

  • 这里展示322.零钱兑换:让找的零钱数最少

    • 假设 f(n) 代表要凑齐金额为 n 所要用的最少硬币数量,那么有:

      f ( n ) = m i n ( f ( n − c 1 ) , f ( n − c 2 ) , . . . f ( n − c n ) ) + 1 f(n) = min(f(n - c1), f(n - c2), ... f(n - cn)) + 1 f(n)=min(f(nc1),f(nc2),...f(ncn))+1

      其中 c1 ~ cn 为硬币的所有面额。

    • 再具体解释一下这个公式吧,例如这个示例:

      输入: coins = [1, 2, 5], amount = 11
      输出: 3 
      解释: 11 = 5 + 5 + 1
      

      题目求的值为 f(11),第一次选择硬币时我们有三种选择。

      假设我们取面额为 1 的硬币,那么接下来需要凑齐的总金额变为 11 - 1 = 10,即 f(11) = f(10) + 1,这里的 +1 就是我们取出的面额为 1 的硬币。

      同理,如果取面额为 2 或面额为 5 的硬币可以得到:

      • f(11) = f(9) + 1
      • f(11) = f(6) + 1

      所以:

      f(11) = min(f(10), f(9), f(6)) + 1
      
    • 参考:力扣-江不知

    class Solution {
    public:
        int coinChange(vector<int>& coins, int amount) {
            vector<int> dp(amount + 1, INT_MAX);
            dp[0] = 0;
            
            // 两种遍历方式都可以
            for (int i = 1; i <= amount; ++ i) {
                for (auto c: coins) {
                    if (i >= c && dp[i-c] != INT_MAX) {
                        dp[i] = min(dp[i], dp[i-c] + 1);
                    }
                }
            }
    
            // for (int c: coins) {
            //     for(int j = c; j <= amount; ++ j) {
            //         if(dp[j- c] != INT_MAX) {
            //             dp[j] = min(dp[j], dp[j-c] + 1);
            //         }
            //     }
            // }
    
            return dp[amount] == INT_MAX ? -1 : dp[amount]; 
        }
    };
    

2. Trick

参考:力扣-澜山

不过好像不是很理解,🤒

背包问题技巧:

  • 如果是0-1背包,即数组中的元素不可重复使用,nums放在外循环,target在内循环,且内循环倒序;

    for num in nums:
        for i in range(target, nums-1, -1):
    
  • 如果是完全背包,即数组中的元素可重复使用,nums放在外循环,target在内循环。且内循环正序。

    for num in nums:
        for i in range(nums, target+1):
    
  • 如果组合问题需考虑元素之间的顺序,需将target放在外循环,将nums放在内循环。

    for i in range(1, target+1):
        for num in nums:
    

3. 所有的背包问题汇总

引用来自公众号「宫水三叶的刷题日记」

  1. 01背包 : 背包问题 第一讲
    【练习】01背包 : 背包问题 第二讲(416. 分割等和子集)
    【学习&练习】01背包 : 背包问题 第三讲(416. 分割等和子集)
  2. 完全背包 : 背包问题 第四讲
    【练习】完全背包 : 背包问题 第五讲(279. 完全平方数)
    【练习】完全背包 : 背包问题 第六讲(322. 零钱兑换)
    【练习】完全背包 : 背包问题 第七讲(518. 零钱兑换 II)
    【练习】完全背包 : 背包问题 第 * 讲(1449. 数位成本和为目标值的最大数字)
  3. 多重背包 : 背包问题 第八讲
    多重背包(优化篇)
    多重背包(优化篇): 背包问题 第九讲
    多重背包(优化篇): 背包问题 第十讲
  4. 混合背包
    【练习】混合背包
    分组背包
    【练习】分组背包
  5. 多维背包
    【练习】多维背包 : 背包问题 第 * 讲(474. 一和零)
    【练习】多维背包 : 背包问题 第 * 讲(879. 盈利计划)
  6. 树形背包
    【练习】树形背包
  7. 背包求方案数
    【练习】背包求方案数 : 背包问题 第 * 讲(494. 目标和)
    【练习】背包求方案数 : 背包问题 第 * 讲(879. 盈利计划)
  8. 背包求具体方案
    【练习】背包求具体方案 : 背包问题 第 * 讲(1049. 最后一块石头的重量 II)
    【练习】背包求具体方案 : 背包问题 第 * 讲(1449. 数位成本和为目标值的最大数字)
  9. 泛化背包
    【练习】泛化背包

4. 背包问题模板🔥🚀

4.1 首先是背包分类的模板:

1、0/1背包:外循环nums,内循环target,target倒序且target>=nums[i];
2、完全背包:外循环nums,内循环target,target正序且target>=nums[i];
3、组合背包(考虑顺序):外循环target,内循环nums,target正序且target>=nums[i];
4、分组背包:这个比较特殊,需要三重循环:外循环背包bags,内部两层循环根据题目的要求转化为1,2,3三种背包类型的模板

4.2 然后是问题分类的模板:

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];

作者:eh-xing-qing
链接:https://leetcode-cn.com/problems/coin-change/solution/yi-pian-wen-zhang-chi-tou-bei-bao-wen-ti-sq9n/
来源:力扣(LeetCode)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值