同步在:博客地址
文章目录
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[i−1][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[i−1][j−w[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[i−1][j],dp[i−1][j−w[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[i−1][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][j−w[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[i−1][j],dp[i][j−w[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(n−c1),f(n−c2),...f(n−cn))+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. 所有的背包问题汇总
- 01背包 : 背包问题 第一讲
【练习】01背包 : 背包问题 第二讲(416. 分割等和子集)
【学习&练习】01背包 : 背包问题 第三讲(416. 分割等和子集) - 完全背包 : 背包问题 第四讲
【练习】完全背包 : 背包问题 第五讲(279. 完全平方数)
【练习】完全背包 : 背包问题 第六讲(322. 零钱兑换)
【练习】完全背包 : 背包问题 第七讲(518. 零钱兑换 II)
【练习】完全背包 : 背包问题 第 * 讲(1449. 数位成本和为目标值的最大数字) - 多重背包 : 背包问题 第八讲
多重背包(优化篇)
多重背包(优化篇): 背包问题 第九讲
多重背包(优化篇): 背包问题 第十讲 - 混合背包
【练习】混合背包
分组背包
【练习】分组背包 - 多维背包
【练习】多维背包 : 背包问题 第 * 讲(474. 一和零)
【练习】多维背包 : 背包问题 第 * 讲(879. 盈利计划) - 树形背包
【练习】树形背包 - 背包求方案数
【练习】背包求方案数 : 背包问题 第 * 讲(494. 目标和)
【练习】背包求方案数 : 背包问题 第 * 讲(879. 盈利计划) - 背包求具体方案
【练习】背包求具体方案 : 背包问题 第 * 讲(1049. 最后一块石头的重量 II)
【练习】背包求具体方案 : 背包问题 第 * 讲(1449. 数位成本和为目标值的最大数字) - 泛化背包
【练习】泛化背包
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)