【LeetCode】17.动态规划系列——背包问题

总目录:

LeetCode系列导航目录

 

0.理论基础

0.1.背包问题体系结构

参考资料:https://leetcode.cn/problems/last-stone-weight-ii/solution/yi-pian-wen-zhang-chi-tou-bei-bao-wen-ti-5lfv/

背包问题:给定一个背包容量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集合,求该背包能装入的最大价值。

链接:https://github.com/hitwzy/leetcode-master/blob/master/problems/%E8%83%8C%E5%8C%85%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%8001%E8%83%8C%E5%8C%85-1.md

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 }
View Code

 滚动数组优化版

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 };
View Code

 

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 };
View Code

 

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 };
View Code

 

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 };
View Code

 

6.单纯的完全背包问题

6.1.问题描述

有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

链接:https://github.com/hitwzy/leetcode-master/blob/master/problems/%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80%E5%AE%8C%E5%85%A8%E8%83%8C%E5%8C%85.md

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 }
View Code

 

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 };
View Code

 

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 };
View Code

 

9.爬楼梯

9.1.问题描述

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

链接: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 };
View Code

 

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 };
View Code

 

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 };
View Code

 

12.纯多重背包问题

12.1.问题描述

有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。

链接:https://github.com/hitwzy/leetcode-master/blob/master/problems/%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80%E5%A4%9A%E9%87%8D%E8%83%8C%E5%8C%85.md

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 }
View Code

 

 

13.总结

13.1.参考资料

链接:背包问题总结

13.2.图谱

 

posted @ 2022-12-28 15:45  啊原来是这样呀  阅读(235)  评论(0编辑  收藏  举报