【LeetCode】17.动态规划系列——背包问题
总目录:
0.理论基础
0.1.背包问题体系结构
背包问题:给定一个背包容量target,再给定一个数组nums(物品),能否按一定方式选取nums中的元素得到target
注意:
(1)背包容量target和物品nums的类型可能是数,也可能是字符串
(2)target可能题目已经给出(显式),也可能是需要我们从题目的信息中挖掘出来(非显式)(常见的非显式target比如sum/2等)
(3)选取方式有常见的一下几种:每个元素选一次/每个元素可选多次/选元素进行排列组合;
选取角度:
(1)01背包问题:每个元素最多选取一次
(2)完全背包问题:每个元素可以重复选择
(3)多重背包问题:相对于01背包问题和完全背包问题,只是每个物品可以不止一个且指定了数量上限,只需要平铺数量转为01背包即可
目标角度:
(1)最值问题:求最大值/最小值,状态转移方程,
(2)存在问题:是否存在…………,满足…………,
(3)组合问题:求所有满足……的排列组合,
目标角度对应的状态转移方程模板:
(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];
0.2.滚动数组优化
0.3.实例
01背包问题:下方问题1
完全背包问题:下方问题6
多重背包问题:下方问题12
1.单纯01背包问题
1.1.问题描述
背包的容量为bagWeight,一堆物品的重量为weight集合、价值为value集合,求该背包能装入的最大价值。
1.2.要点
01背包问题、求极值问题,
对于物品i可选装入或者不装入,
状态转移方程dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i])
1.3.代码实例
二维dp
1 void test_2_wei_bag_problem1() { 2 vector<int> weight = {1, 3, 4}; 3 vector<int> value = {15, 20, 30}; 4 int bagweight = 4; 5 6 // 二维数组 7 vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0)); 8 9 // 初始化 10 for (int j = weight[0]; j <= bagweight; j++) { 11 dp[0][j] = value[0]; 12 } 13 14 // weight数组的大小 就是物品个数 15 for(int i = 1; i < weight.size(); i++) { // 遍历物品 16 for(int j = 0; j <= bagweight; j++) { // 遍历背包容量 17 if (j < weight[i]) dp[i][j] = dp[i - 1][j]; 18 else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 19 20 } 21 } 22 23 cout << dp[weight.size() - 1][bagweight] << endl; 24 } 25 26 int main() { 27 test_2_wei_bag_problem1(); 28 }
滚动数组优化版
1 for(int i = 0; i < weight.size(); i++) { // 遍历物品 2 for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量 3 dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); 4 } 5 }
2.分割等和子集
2.1.问题描述
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
链接:https://leetcode.cn/problems/partition-equal-subset-sum/
2.2.要点
数学建模:能否找出一个子集其和为sum/2
01背包问题、存在问题
对于物品i,可不存在、可存在
状态转移方程:dp[j]=dp[j] || dp[j-weight[i]]
2.3.代码实例
1 class Solution { 2 public: 3 bool canPartition(vector<int>& nums) { 4 int dataLen=nums.size(); 5 int sum = 0; 6 7 for (int i = 0; i < dataLen; i++) { 8 sum += nums[i]; 9 } 10 // 也可以使用库函数一步求和 11 // int sum = accumulate(nums.begin(), nums.end(), 0); 12 if (sum % 2 == 1) return false; 13 int target = sum / 2; 14 15 // 开始 01背包 16 // dp[i]中的i表示背包内总和 17 vector<bool> dp(target+1,false); 18 // 先填表格第 0 行,第 1 个数只能让容积为它自己的背包恰好装满 19 if (nums[0] <= target) { 20 dp[nums[0]] = true; 21 } 22 for(int i=1;i<dataLen;i++){ 23 for(int j=target;j>=nums[i];j--){ 24 //如果在中间过程就找到。,立即退出 25 if(dp[target]){ 26 return true; 27 } 28 29 dp[j]=dp[j]||dp[j-nums[i]]; 30 } 31 } 32 33 return dp[target]; 34 } 35 };
3.最后一块石头的重量之随意对撞
3.1.问题描述
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
(1)如果 x == y,那么两块石头都会被完全粉碎;
(2)如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
链接:https://leetcode.cn/problems/last-stone-weight-ii
3.2.要点
数学建模:尽量将整个集合二等分,使得两个子集合之差最小
01背包问题、极值问题
状态转移方程:dp[j]=max(dp[j],dp[j-stones[i]]+stones[i]);
3.3.代码实例
1 class Solution { 2 public: 3 int lastStoneWeightII(vector<int>& stones) { 4 int sum=0; 5 for(int& num:stones){ 6 sum+=num; 7 } 8 int target=sum/2; 9 vector<int> dp(target+1,0);//dp[n]容量为n的背包最大能背多少重量 10 for(int i=0;i<stones.size();i++){ 11 for(int j=target;j>=stones[i];j--){ 12 dp[j]=max(dp[j],dp[j-stones[i]]+stones[i]); 13 } 14 } 15 16 return sum-dp[target]-dp[target]; 17 } 18 };
4.目标和:集合内加加减减得到指定值
4.1.问题描述
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
链接:https://leetcode.cn/problems/target-sum
4.2.要点
数学建模:集合总和sum可计算得到,将集合分为A和sum-A两部分,则A-(sum-A)=target,A=(sum+target)/2,问题转变为使得总和为A的子集合的数量
01背包问题、组合问题
状态转移方程:dp[j]+=dp[j-nums[i]];注意初始值dp[0]=1
这题有一定的特殊性,特殊在于物品价值存在为0的情况,导致遍历范围有点特殊
4.3.代码实例
1 class Solution { 2 public: 3 int findTargetSumWays(vector<int>& nums, int target) { 4 int sum=0; 5 for(int& i:nums) sum+=i; 6 if(target>sum) return 0; 7 sum+=target; 8 if((sum%2)!=0) return 0; 9 10 int tgtSum=sum/2; 11 if(tgtSum<0){ 12 return 0; 13 } 14 vector<int> dp(tgtSum+1,0);//dp[n]组合成n有dp[n]种方法 15 dp[0]=1; 16 for(int i=0;i<nums.size();i++){ 17 for(int j=tgtSum;j>=nums[i];j--){ 18 dp[j]+=dp[j-nums[i]]; 19 } 20 } 21 22 return dp[tgtSum]; 23 } 24 };
5.集合中0和1的数量
5.1.问题描述
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
示例:输入:strs = ["10", "0", "1"], m = 1, n = 1 输出:2 解释:最大的子集是 {"0", "1"} ,所以答案是 2 。
链接:https://leetcode.cn/problems/ones-and-zeroes
5.2.要点
如果只看0的数量,则是基本的最大价值问题,这里是附加要求考虑1的数量。
01背包、极值问题,只不过这个极值是多维的
状态转移方程:dp[j][k] = max(dp[j][k], dp[j - zeros[i]][k - ones[i]] + 1);
01背包问题:zeroCnt、oneCnt相当于重量,字符串的个数相当于价值
5.3.代码实例
1 class Solution { 2 public: 3 int findMaxForm(vector<string>& strs, int m, int n) { 4 //统计每个字符串中0/1次数 5 int len=strs.size(); 6 vector<int> zeros(len,0); 7 vector<int> ones(len,0); 8 for(int i=0;i<len;i++){ 9 for(auto& c:strs[i]){ 10 if(c=='0'){ 11 zeros[i]++; 12 } 13 else{ 14 ones[i]++; 15 } 16 } 17 } 18 19 int temp=0; 20 vector<vector<int>> dp(m+1,vector<int>(n+1,0));//dp[i][j]是zero容量为m、one容量为n时的最大值 21 for(int i=0;i<len;i++){ 22 for(int j=m;j>=zeros[i];j--){ 23 for(int k=n;k>=ones[i];k--){ 24 dp[j][k] = max(dp[j][k], dp[j - zeros[i]][k - ones[i]] + 1); 25 } 26 } 27 } 28 29 return dp[m][n]; 30 } 31 };
6.单纯的完全背包问题
6.1.问题描述
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
6.2.要点
与01背包的区别之处就在于每个物品可以取无限次。
在遍历时,01背包中为了避免重复装入而采取的倒序遍历背包容量的操作。
在完全背包中物品是可以添加多次的,所以遍历背包容量要从小到大去遍历,因为本行信息不必只来自于上一行,本行前面的内容仍可以有更新的机会。
至于先遍历物品还是先遍历包容量,取决于目标是求极值求存在还是求方案个数。
求极值、求存在则物品、包容量遍历顺序任意。
如果求方案数量,则要看是否对顺序敏感。排列是对顺序敏感,组合是对顺序不敏感。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
可以通过dp表予以验证:
6.3.代码实例
1 // 先遍历物品,在遍历背包 2 void test_CompletePack() { 3 vector<int> weight = {1, 3, 4}; 4 vector<int> value = {15, 20, 30}; 5 int bagWeight = 4; 6 vector<int> dp(bagWeight + 1, 0); 7 for(int i = 0; i < weight.size(); i++) { // 遍历物品 8 for(int j = weight[i]; j <= bagWeight; j++) { // 遍历背包容量 9 dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); 10 } 11 } 12 cout << dp[bagWeight] << endl; 13 } 14 int main() { 15 test_CompletePack(); 16 }
7.零钱兑换——求组合
7.1.问题描述
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
链接:https://leetcode.cn/problems/coin-change-ii
7.2.要点
完全背包问题,求组合
7.3.代码实例
1 class Solution { 2 public: 3 int change(int amount, vector<int>& coins) { 4 int len=coins.size(); 5 int ret=0; 6 7 vector<int> dp(amount+1,0); 8 dp[0]=1; 9 for(int i=0;i<len;i++){ 10 for(int j=coins[i];j<=amount;j++){ 11 dp[j]+=dp[j-coins[i]]; 12 } 13 } 14 15 return dp[amount]; 16 } 17 };
8.零钱兑换——求极值
8.1.问题描述
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
链接:https://leetcode.cn/problems/coin-change
8.2.要点
完全背包,求极值
8.3.代码实例
1 class Solution { 2 public: 3 int coinChange(vector<int>& coins, int amount) { 4 int len=coins.size(); 5 vector<long> dp(amount+1,INT_MAX);//dp[n],凑够n所需的最少硬币数量 6 dp[0]=0; 7 8 //完全背包、组合问题 9 for(int i=0;i<len;i++){ 10 for(int j=coins[i];j<=amount;j++){ 11 if (dp[j - coins[i]] != INT_MAX) // 如果dp[j - coins[i]]是初始值则跳过 12 dp[j]=min(dp[j],dp[j-coins[i]]+1); 13 } 14 } 15 16 return dp[amount]==INT_MAX?-1:dp[amount]; 17 } 18 };
9.爬楼梯
9.1.问题描述
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
链接:https://leetcode.cn/problems/climbing-stairs/
9.2.要点
完全背包,求排列
9.3.代码实例
1 class Solution { 2 public: 3 int climbStairs(int n) { 4 vector<int> dp(n+1,0); 5 dp[0]=1; 6 7 //完全背包的排列问题 8 for(int i=0;i<=n;i++){ 9 for(int j=1;j<=2;j++){ 10 if(i>=j){ 11 dp[i]+=dp[i-j]; 12 } 13 } 14 } 15 16 return dp[n]; 17 } 18 };
10.完全平方数的组合
10.1.问题描述
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例:输入:n = 12
输出:3 解释:12 = 4 + 4 + 4
链接:https://leetcode.cn/problems/perfect-squares
10.2.要点
完全背包,求极值
10.3.代码实例
1 class Solution { 2 public: 3 int numSquares(int n) { 4 vector<int> dp(n+1,INT_MAX); 5 dp[0]=0; 6 7 int tempSq=0; 8 for(int i=1;i<=100;i++){//平方根的范围是1~100,先遍历物品 9 tempSq=i*i; 10 for(int j=tempSq;j<=n;j++){//遍历背包 11 if(dp[j-tempSq]!=INT_MAX) 12 dp[j]=min(dp[j],dp[j-tempSq]+1); 13 } 14 } 15 16 return dp[n]; 17 } 18 };
11.单词能否拆分
11.1.问题描述
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
链接:https://leetcode.cn/problems/word-break
11.2.要点
完全背包,求存在
虽然完全背包求存在问题,先物品还是先背包都是可以的,但这题还是先背包方便一些。原因可参考LC上的题解。
11.3.代码实例
1 class Solution { 2 public: 3 bool wordBreak(string s, vector<string>& wordDict) { 4 int len=s.length(); 5 int wordCnt=wordDict.size(); 6 unordered_set<string> strSet(wordDict.begin(),wordDict.end());//存入哈希表 7 8 vector<bool> dp(len+1,false);//dp[i],前i个字符能否由字典构成 9 dp[0]=true; 10 string tmp; 11 int curStrLen=0; 12 for(int i=1;i<=len;i++){//遍历背包 13 for(int j=0;j<wordCnt;j++){ 14 curStrLen=wordDict[j].length(); 15 if(i<curStrLen) continue; 16 tmp=s.substr(i-curStrLen,curStrLen); 17 if(strSet.find(tmp)==strSet.end()) continue; 18 19 //容量够、字典里有 20 dp[i]=dp[i]||dp[i-curStrLen]; 21 } 22 } 23 24 return dp[len]; 25 } 26 };
12.纯多重背包问题
12.1.问题描述
有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。
12.2.要点
例如背包容量为10,物品信息如下:
转为01背包的物品信息:
此时只需要解01背包的极值问题
12.3.代码实例
1 void test_multi_pack() { 2 vector<int> weight = {1, 3, 4}; 3 vector<int> value = {15, 20, 30}; 4 vector<int> nums = {2, 3, 2}; 5 int bagWeight = 10; 6 for (int i = 0; i < nums.size(); i++) { 7 while (nums[i] > 1) { // nums[i]保留到1,把其他物品都展开 8 weight.push_back(weight[i]); 9 value.push_back(value[i]); 10 nums[i]--; 11 } 12 } 13 14 vector<int> dp(bagWeight + 1, 0); 15 for(int i = 0; i < weight.size(); i++) { // 遍历物品 16 for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量 17 dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); 18 } 19 for (int j = 0; j <= bagWeight; j++) { 20 cout << dp[j] << " "; 21 } 22 cout << endl; 23 } 24 cout << dp[bagWeight] << endl; 25 26 } 27 int main() { 28 test_multi_pack(); 29 }
13.总结
13.1.参考资料
链接:背包问题总结
13.2.图谱