当前位置:   article > 正文

二刷力扣——DP算法(背包问题)_背包问题力扣

背包问题力扣

本文截图来自力扣官方题解、评论区其他题解 以及 代码随想录。

如果某一问题有很多重叠子问题,使用动态规划是最有效的。

动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。

DP五步曲 :

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

70. 爬楼梯

1、省略列出上面的五部。

确定递推公式:

所以递推公式和斐波那契数列一样,但是递归的话会超时,所以遍历推导:

之后确定递推公式,很多都是看第n步之前的第n-1步的情况,然后来分类讨论第n步怎么根据第n-1步的情况得到值。

2、学习新方法:矩阵快速幂。

这里M的构造,使得(f(n+1),f(n))能够转换成各自都下标减一的向量,这是矩阵的 dp数组。从而把下标较小的那一个向量再转换为M×下标更小的向量,最后会递推成(f(n+1),f(n))与( f(1),f(0) )的关系。

思考题:

一般方法应该是待定系数法把f(x)后面的 推出来。

代码如下。

注意求a的n次方的函数matrixPow 。不是计算n次,而是每一次都计算平方,如果是奇数,就需要单独乘一个矩阵。比如5:一开始5&1是1,所以ret=Ea=m;然后a变成了m^2,5右移变成2,if里面不成立,a再平方变成m^4,2右移变成1;if里面成立,结果=ret * a=m * m^4。以后可能还能用到。

  1. class Solution {
  2. public:
  3. //矩阵乘法
  4. vector<vector<long long>> multiply(vector<vector<long long>> &a,vector<vector<long long>>& b)
  5. {
  6. vector<vector<long long>> c(2, vector<long long>(2));
  7. for(int i=0;i<2;++i)
  8. {
  9. for(int j=0;j<2;++j)
  10. {
  11. c[i][j]=a[i][0]*b[0][j]+a[i][1]*b[1][j];
  12. }
  13. }
  14. return c;
  15. }
  16. vector<vector<long long>> matrixPow(vector<vector<long long>> a,int n)
  17. {
  18. vector<vector<long long>> ret = {{1, 0}, {0, 1}};
  19. while(n>0)
  20. {
  21. if((n&1)==1)//是奇数
  22. {
  23. ret=multiply(ret,a);
  24. }
  25. a=multiply(a,a);//a^2、a^4……
  26. n >>= 1; //除2
  27. }
  28. return ret;
  29. }
  30. int climbStairs(int n) {
  31. vector<vector<long long>> m={{1,1},{1,0}};//m矩阵
  32. vector<vector<long long>> res=matrixPow(m,n);//求m的n次方
  33. return res[0][0];
  34. }
  35. };

时间复杂度:O(logn)

3、还有一种得到通项公式了直接带入 n 求到结果的:

但是对于常微分方程什么的高数知识有些忘记了。

从力扣这篇题解学到了很多东西:

746.最小花费爬楼梯

跟上面一样,dp表示到第i个台阶的最小花费,最后台阶下标是n。

初始化的话:

当i>=2,上一步有可能爬两步,也可能一步,所以递推公式:

  1. class Solution {
  2. public:
  3. int minCostClimbingStairs(vector<int>& cost) {
  4. int n = cost.size();
  5. vector<int> dp(n+1,0);//到i个台阶的花费
  6. // dp[0]=0;
  7. // dp[1]=0;
  8. for(int i=2;i<=cost.size();++i)
  9. {
  10. dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
  11. }
  12. return dp[n];
  13. }
  14. };

上述代码的时间复杂度和空间复杂度都是 O(n)。注意到当 i≥2时,dp[i]只和 dp[i−1] 与 dp[i−2] 有关,因此可以使用滚动数组的思想,将空间复杂度优化到 O(1)。

  1. class Solution {
  2. public:
  3. int minCostClimbingStairs(vector<int>& cost) {
  4. int n = cost.size();
  5. int next,cur=0,pre=0;
  6. for(int i=2;i<=cost.size();++i)
  7. {
  8. next=min(cur+cost[i-1],pre+cost[i-2]);
  9. pre=cur;
  10. cur=next;
  11. }
  12. return next;
  13. }
  14. };

以后也会用滚动数组代替dp本身,要先会dp数组的实现了,再实现滚动数组。

62. 不同路径

得是二维dp了,dp[i][j]:(i,j)到终点的路径条数。

因为只能两个方向移动,所以是在(i-1,j)和(i,j-1)的基础上得到(i,j)的。

这里把终点和起点调换了一下,想让遍历顺序从左到右,从上到下而已。(左右和上下的顺序谁在前都可以)

注意初始化了一行+一列。

  1. class Solution {
  2. public:
  3. int uniquePaths(int m, int n) {
  4. vector<vector<int>> dp(m,vector<int>(n,0));
  5. for(int i=0;i<m;++i)dp[i][0]=1;
  6. for(int j=1;j<n;++j)dp[0][j]=1;
  7. for(int i=1;i<m;++i)
  8. {
  9. for(int j=1;j<n;++j)
  10. {
  11. dp[i][j]=dp[i-1][j]+dp[i][j-1];//只能向上/左
  12. }
  13. }
  14. return dp[m-1][n-1];
  15. }
  16. };

还可以用滚动数组,一维代替二维。只是省略了空间,但是还是两个循环的动态规划,只不过i不参与到值的推导中。

  1. class Solution {
  2. public:
  3. int uniquePaths(int m, int n) {
  4. vector<int> dp(n,0);
  5. // for(int i=0;i<m;++i)dp[i][0]=1;
  6. for(int j=0;j<n;++j)dp[j]=1;
  7. for(int i=1;i<m;++i)
  8. {
  9. for(int j=1;j<n;++j)
  10. {
  11. dp[j]+=dp[j-1];//只能向上/左
  12. }
  13. }
  14. return dp[n-1];
  15. }
  16. };

时间复杂度:O(m*n)

空间复杂度:O(n)

也可以省略n这个维度,空间复杂度变O(m)。

  1. class Solution {
  2. public:
  3. int uniquePaths(int m, int n) {
  4. vector<int> dp(m,0);
  5. for(int i=0;i<m;++i)dp[i]=1;
  6. // for(int j=0;j<n;++j)dp[j]=1;
  7. for(int j=1;j<n;++j)
  8. {
  9. for(int i=1;i<m;++i)
  10. {
  11. dp[i]+=dp[i-1];
  12. }
  13. }
  14. return dp[m-1];
  15. }
  16. };

注意一维的时候,这个参与推导的维度得在内循环,要经历n轮的 dp[0]到dp[m-1] 才能得到最终的dp[m-1]。

63. 不同路径 II

跟上面的区别是遇到障碍的话dp[i][j]直接变0了。

注意初始化的时候遇到障碍物就退出了,障碍物之后的dp都是0.

二维:

  1. class Solution {
  2. public:
  3. int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
  4. int m=obstacleGrid.size();
  5. int n=obstacleGrid[0].size();
  6. vector<vector<int>> dp(m,vector<int>(n,0));
  7. for(int i=0;i<m;++i){
  8. if(obstacleGrid[i][0]==1)break;
  9. dp[i][0]=1;
  10. }
  11. for(int i=0;i<n;++i){
  12. if(obstacleGrid[0][i]==1)break;
  13. dp[0][i]=1;
  14. }
  15. for(int i=1;i<m;++i)
  16. {
  17. for(int j=1;j<n;++j)
  18. {
  19. if(obstacleGrid[i][j]==1)dp[i][j]=0;
  20. else dp[i][j]=dp[i-1][j]+dp[i][j-1];//只能向上/左
  21. }
  22. }
  23. return dp[m-1][n-1];
  24. }
  25. };

一维:

  1. class Solution {
  2. public:
  3. int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
  4. int m=obstacleGrid.size();
  5. int n=obstacleGrid[0].size();
  6. vector<int> dp(m);
  7. for(int i=0;i<m;++i){
  8. if(obstacleGrid[i][0]==1)break;
  9. dp[i]=1;
  10. }
  11. for(int j=1;j<n;++j)
  12. {
  13. for(int i=0;i<m;++i)
  14. {
  15. if(obstacleGrid[i][j]==1)dp[i]=0;//障碍处
  16. else { //没有障碍
  17. if(i>0)dp[i]+=dp[i-1];//[[0,1],[0,0]]
  18. }
  19. }
  20. }
  21. return dp[m-1];
  22. }
  23. };

注意这里一维的话,因为没有给另一个维度的第一行/列初始化,所以这里i 得从0开始。举例可知:[[0,1],[0,0]]

循环体里面分为有障碍和没障碍。必须在二维的基础上来调整,虽然简略但是解释性不是很好。

思想同下。

343. 整数拆分

1、DP

dp[i]:正整数i拆分 得到的最大乘积。

可以分成两种情况:①n拆分成2个正整数的和。②n拆分成大于2个正整数的和。

①的话,dp[n]应该=j*(n-j)的最大值,②的话,dp[n]应该等于j*dp[n-j]的最大值。j是从1遍历到i-1,因为dp[n-j]是分解n-j这个数得到的最大的乘积,所以至少分解了2次,乘j就是至少分解了3次。

所以dp[n]应该取两种情况下的最大值,dp[n]=max(  j * ( n-j ), j * dp[n-j] )。这个dp[n]只是n包含j的时候分解的最大值(只考虑了右边的最大值),和前面的n包含1……j-1的时候分解的最大值没有联系起来,所以这个式子还是不对的。

因此dp[n]还要和自己比较,和之前的j对应的最大值(也就是最近一次更新的dp[n]比较),最终才是最大值。也就是左边右边都要考虑

  1. class Solution {
  2. public:
  3. int integerBreak(int n) {
  4. vector<int> dp(n+1,0);
  5. dp[2]=1;
  6. for(int i=3;i<=n;++i)
  7. {
  8. int curmax=INT_MIN;
  9. for(int j=1;j<i;++j)
  10. {
  11. curmax=max(curmax,j*max((i-j),dp[i-j]));//在左边是1到i-1的基础上,右边2种情况:仅一个数/再划分。取最大值
  12. }
  13. dp[i]=curmax;//确定dp[i]
  14. }
  15. return dp[n];
  16. }
  17. };

2、数学解决:

对于大于 4的正整数,总是存在一种拆分的方案,使得拆分成的两个正整数的乘积大于拆分前的正整数。

力扣题解用导数和归纳证明法证明的。

简单来说是:

因为大于4的整数f,都可以分解为2+f-2,而2(f-2)肯定比f大,所以有大于4的整数,就分解他。所以乘积式子 左边只会出现 1、2、3。×1的话会变小所以不考虑,那左边只会有2和3。

2和3的个数怎么确定?答:

所以所有情况就是:若干个3和 两个/一个/没有 2。所以 把 n求余。余1 是4的倍数,即若干个3和两个2.其他同理。

  1. class Solution {
  2. public:
  3. int integerBreak(int n) {
  4. //数学
  5. if(n<4)return n-1;
  6. int mi=n/3;
  7. int re=n%3;
  8. if(re==0)
  9. {
  10. return pow(3,mi);
  11. }
  12. else if(re==1)//2个2
  13. {
  14. return pow(3,mi-1)*4;
  15. }
  16. return pow(3,mi)*2;//1个2
  17. }
  18. };

96. 不同的二叉搜索树

恰由 n 个节点组成且节点值从 1n 互不相同的 二叉搜索树:可以遍历每个数字 i,将该数字作为树根,将 1⋯(i−1) 序列作为左子树,将 (i+1)⋯n 序列作为右子树。接着可以按照同样的方式递归构建左子树和右子树。

左子树和右子树的节点数肯定小于n,所以先得到小于n的数量,再从而得到n个节点的数量。典型用动态规划。

dp[i]:恰由 i 个节点组成且节点值从 1i 互不相同的 二叉搜索树 数目。

递推公式:所以以左子树为标准:左子树节点个数可以从1到n-1,其不同搜索树 数目为dp[1]——dp[n-1]。那么右子树节点数量就是i-1-左子树节点个数,不同搜索树 数目同理。

初始化:dp[1]一个节点的情况也可以由循环得出(i从1 开始)

  1. class Solution {
  2. public:
  3. int numTrees(int n) {
  4. vector<int> dp(n+1,0);
  5. dp[0]=1;
  6. dp[1]=1;
  7. for(int i=2;i<=n;++i)
  8. {
  9. for(int j=0;j<i;++j)
  10. {
  11. dp[i]+=dp[j]*dp[i-j-1];//左子树j个;右子树i-j-1 个
  12. }
  13. }
  14. return dp[n];
  15. }
  16. };

和上面 整数拆分 是一样:

要得到每一个dp[i],还需要一层内循环来 统计 最大值/总数。不同路径是显式地说明了用两层循环,这两道题是隐式的。

相同点是 dp[i]都由dp[1]——dp[i-1] 和 前面几轮的 dp[i]自己 得到,这是一个二维的过程,两个不同路径 的二维dp 简化成了 一维滚动数组;而这里本身就是 一维数组。区别是存储空间的大小,相同的是 二维的过程、时间。

开始背包:

46. 携带研究材料(第六期模拟笔试)

dp数组的定义是需要细想的,确定了之后是需要牢记贯穿全题的。

1、定义:

dp[i][j]:从【0,i】范围内随便选材料,使得重量是j,dp[i][j]是最大价值。

所以前一个下标范围 是[0,m-1](假设m个物品),后一个下标范围是[0,n]。假设 n 是背包可承受最大重量/空间.

重量这个维度从0开始,列从第一个物品开始。

2、初始化:

联系实际,重量为0的时候价值肯定也为0,所以第一列是0。

选第一个物品的时候,当这个物品重量比此时的背包重量 j 还要大,那就不能放这个物品,值为0;≤的话可以放。

所以就初始化j>=weights[0] 的情况:

  1. // 初始化
  2. for(int i=weights[0];i<=n ;++i)dp[0][i]=values[0];

3、递推公式:

得到dp[i][j]有2种情况:

①一定不能放物品 i。如果物品i放进重量为j的背包会超重,那么不能把i放进去,所以重量设定为j的话,从[0,i-1]选择和从[0,i]选择的最大价值是一样,所以 dp[i][j] = dp[i-1][j] 。

②可放可不放物品 i。如果不会超重,可以选择是否放进去。

物品 i 放进去,背包最大重量为 j ,还没有放进去物品 i 的时候,背包的最大重量为 j - weight[i] ,数量为i-1,所以这时背包的最大价值为 dp[ i-1 ][ j - weight[i] ]。加入物品 i ,背包价值 dp[i][j] 就为 dp[ i-1 ][ j - weight[i] ] + value[i] 。

不放进去和前面①一样。

取两者最大值:

所以dp[i][j]= max ( dp[i-1][j] , dp[ i-1 ][ j - weight[i] ] + value[i] )。

4、遍历顺序:

观察到dp[ i ]d [ j ] 是由统一左上角的值推来的。所以遍历顺序只要满足在计算dp[ i ]d [ j ] 的时候左上角的值都已经得到了就行。所以先行还是先列都是可以的。

  1. #include<vector>
  2. #include<iostream>
  3. using namespace std;
  4. int main()
  5. {
  6. int m,n;
  7. cin>>m>>n;
  8. vector<int> weights(m);
  9. vector<int> values(m);
  10. for(int i=0;i<m;++i)
  11. {
  12. cin>>weights[i];
  13. }
  14. for(int i=0;i<m;++i)
  15. {
  16. cin>>values[i];
  17. }
  18. vector<vector<int>> dp(m,vector<int>(n+1,0));//从【0,i】范围内选材料 重量是j的最大价值
  19. // 初始化
  20. for(int i=weights[0];i<=n ;++i)dp[0][i]=values[0];
  21. for(int i=1;i<m;++i)
  22. {
  23. for(int j=0;j<=n;++j)
  24. {
  25. if(j<weights[i])dp[i][j]=dp[i-1][j];//物品i太大一定放不了
  26. else dp[i][j]=max(dp[i-1][j],dp[i-1][j-weights[i]]+values[i]);//可放可不放
  27. }
  28. }
  29. cout<< dp[m-1][n]<<endl;
  30. return 0;
  31. }

同样可以把二维数组转化成一维DP数组 来递推:

1、定义

可以发现递推公式dp[i]只出现在左边,dp[i-1]只出现在右边,所以完全可以省略掉这一个维度,只留下上面二维数组的列。

2、这样递推公式变成:

  1. if(j<weight[i] ) dp[j]=dp[j];
  2. else dp[ j ] = max(dp[ j ], dp[ j - weight[ i ] ] + value[ i ]);

那个if 也可以省略了。变成:

dp[j]=max(dp[j],dp[j-weights[i]]+values[i]);

这是递推公式的变化。

3、重量维度必须倒着推。

在一刷的时候解释的比较清楚了:

为什么背包是倒序遍历?倒序遍历是为了保证物品i只被放入一次。比如上面的例题,物品0的重量weight[0] = 1,价值value[0] = 15。第一轮i=0:先算dp[1]=max(dp[1],dp[0]+15)=15,那么dp[2]=max(dp[2] , dp[1] + 15)=30,dp[2]就包含了2个物品0。肯定是错的。那么考虑反过来,先算dp[4]的话,先算dp[4]=max(dp[4],dp[3]+15)=15,dp[3]=max(dp[3],dp[2]+15)=15,没有重复包含。所以只能倒序遍历。所以倒序遍历的原因是,本质上还是一个对二维数组的遍历,只不过第二行的覆盖第一行,第三行的覆盖第二行……并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。从左到右覆盖左边的值就是这一层更新过的了。

为什么只能先物品再背包?还是那句话:一维遍历本质上还是一个对二维数组的遍历,右下角的值依赖上一层左上角的值,所以需要保证左边的值仍然是上一层的,结合上面的背包倒序遍历,想象这个过程中,dp数组倒序遍历一轮就是更新了之前二维数组的一行,然后下一轮就是更新二维数组的下一行,这样左边的值仍然是上一层的。

如下图。既然没有了i,那么进行第二轮的j=4,j=3的时候,之前一轮的这两个值就会被覆盖,只有以“先物品再背包背包倒序遍历”这个顺序来才能使得dp[j]和二维的dp[i-1][j]是一样的效果,我需要用到的是前一轮的值,那么不能先更新前一轮的值,而是要从后往前更新。所以除了这个情况之外的,都会覆盖原来左上角的值,是不对的。

二维数组的时候,本来就是由上一层的dp[i-1]计算得到的,所以上一层的值直接能拿来用不会被覆盖,所以先物品先背包都可以。

4、初始化。

递推公式只包含j,所以不用初始化第一行。

那么假设物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了。

  1. #include<iostream>
  2. #include<vector>
  3. using namespace std;
  4. void maxValue(){
  5. int m,begweight;//M 代表物品的种类数,begweight是背包容量
  6. cin>>m>>begweight;
  7. vector<int> weight(m);
  8. vector<int> value(m);
  9. for(int i=0;i<m;++i){
  10. cin>>weight[i];
  11. }
  12. for(int i=0;i<m;++i){
  13. cin>>value[i];
  14. }
  15. vector<int> dp(begweight+1,0);
  16. //递推
  17. for(int i=0;i<m;++i){
  18. for(int j=begweight;j>=weight[i];--j){
  19. dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
  20. }
  21. }
  22. cout<< dp[begweight] << endl;
  23. }
  24. int main(){
  25. maxValue();
  26. return 0;
  27. }

根据表格,观察每个元素 依赖哪些位置的 其他元素得到,从而确定遍历顺序。

416. 分割等和子集

同上面0-1背包的模板。weight数组和values数组都是nums。背包能背sum的一半就对。

  1. class Solution {
  2. public:
  3. bool canPartition(vector<int>& nums) {
  4. //背包容量:sum(nums)/2;价值同容量
  5. int sum=0;
  6. for(int num:nums)
  7. {
  8. sum+=num;
  9. }
  10. if(sum%2==1)return false;
  11. int n=sum/2;
  12. vector<int> dp(n+1,0);
  13. for(int i=0;i<nums.size();++i)
  14. {
  15. for(int j=n;j>=nums[i];--j)
  16. {
  17. dp[j]=max(dp[j],dp[j-nums[i]]+nums[i]);
  18. }
  19. }
  20. if(dp[n]==n)return true;
  21. return false;
  22. }
  23. };

1049. 最后一块石头的重量 II

和上道题一样

思想:把若干次两个石头相撞,转化成一次两堆石头相撞。

举例:

[2,7,4,1,8,1]

多次相撞:

组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

总的式子可以表达为:((4-2)-1)+(8-7)+(1-1)+1。最后只剩下1个1,一个减号对应一次相撞。多次相减得到剩下的重量。

式子可以转变成:4+8+1+1-2-1-7。就是把4、8、1分成一堆,其他的是另一堆。一次相撞,就能得到结果。

于是背包容量又是元素总和的一半,尽量装满就行。

最后dp[target] 是最接近一半的一堆,另一堆减去这一堆就是结果。

  1. class Solution {
  2. public:
  3. int lastStoneWeightII(vector<int>& stones) {
  4. int sum=0;
  5. for(int a:stones)sum+=a;
  6. int begweight=sum/2;
  7. vector<int> dp(begweight+1,0);
  8. for(int i=0;i<stones.size();++i){
  9. for(int j=begweight;j>=stones[i];--j){
  10. dp[j]=max(dp[j],dp[j-stones[i]]+stones[i]);
  11. }
  12. }
  13. return (sum-2*dp[begweight]);
  14. }
  15. };

494. 目标和

这里求方案数,不是某个重量对应的包里面的价值

所以定义改为:nums里面随便选num,使得背包里面是j的方法种数。

因为直接二维数组转化成的滚动数组,思考过程还是二维的:第i轮要计算dp[j],那么上一轮i-1轮的dp[j-nums[i]]还没有更新:含义是从[0,i-1] 选物品 ,使得背包里面是j-nums[i] 的方法数①。上一轮的dp[j]:含义是从[0,i-1] 选物品 ,使得背包里面是j 的方法数②。所以第i轮是从[0,i] 选物品,继承了①的,背包里面再放入i物品,根据 数学求排列组合那一块的 加法乘法原理,选了i 的方法数是①;不选物品i,方法数同②,所以这一轮i 的dp[j] 就是两者加起来,考虑两种情况。

这个公式在后面背包解决排列组合问题的时候还会用到。

  1. class Solution {
  2. public:
  3. int findTargetSumWays(vector<int>& nums, int target) {
  4. int sum=0;
  5. for(int a:nums)sum+=a;
  6. if(abs(target)>sum)return 0;
  7. if((sum+target)%2==1)return 0;
  8. //背包容量begweight=left=(sum+target)/2
  9. int begweight=(sum+target)/2;
  10. vector<int> dp(begweight+1,0);
  11. dp[0]=1;
  12. for(int i=0;i<nums.size();++i){
  13. for(int j=begweight;j>=nums[i];--j){
  14. dp[j]+=dp[j-nums[i]];
  15. }
  16. }
  17. return dp[begweight];
  18. }
  19. };

474. 一和零

建立原始dp数组,应该有3个维度:三个维度分别是字符串、0的容量和 1 的容量。

只是比普通背包多了一个维度。普通背包一个物品维度,一个背包维度;这里有两个背包维度。

所以过程两层循环变成三层循环,为了实现滚动数组,还是物品在外循环,两个背包在内循环。

递推公式:

可见去掉维度k和普通背包一模一样。

那么同样省略物品这个维度,变成滚动数组可以节省空间但时间一样:

  1. class Solution {
  2. public:
  3. int findMaxForm(vector<string>& strs, int m, int n) {
  4. vector<vector<int>> dp(m+1,vector<int>(n+1,0)); 在[0,i]里面选,有i个0和j个1的最大长度
  5. for(string str:strs){
  6. int x=0,y=0;
  7. //统计x和y
  8. for(char s:str){
  9. if(s=='0') x++;
  10. else y++;
  11. }
  12. //二维递推
  13. for(int i=m;i>=x;--i){
  14. for(int j=n;j>=y;--j){
  15. dp[i][j]=max(dp[i][j],dp[i-x][j-y]+1);
  16. }
  17. }
  18. }
  19. return dp[m][n];
  20. }
  21. };

初始化、顺序 也是和前面的滚动数组一样的。

518. 零钱兑换 II

1、左右上下顺序:

01背包逆序是为了不重复放入某个物品,因为dp[j]依靠物品 i 这一行左边的值得来,从左往右顺序来的话左边某个值放入了这一行代表的物品i,dp[j]取的是上面递推公式的右边的值,那就会再加一次value[i],也就是又放入一次物品 i。

所以从左往右不符合01背包要求,但是却是符合完全背包的要求的 ,因为物品i有无限多个,对于物品i这行,想放几次就放几次,那么肯定从左往右才能得到完全背包的最大值。完全背包的最大值肯定是比01背包的最大值要大,因为完全背包较大的一些物品的价值可以重复放入,总的价值就会增大,所以要想最大,就得采用从左往右这种可以重复取物品的顺序。

2、物品背包内外顺序:

组合问题的话是先物品还是先背包呢,先物品这种情况,是对于某个物品i,来看逐渐增大的背包是否能装下,所以先在逐渐增大的背包里面看1能不能装下,再看2能不能装下,再看5能不能装下,然后结束。所以只会出现顺序为1,2,5的情况,所以上面输出为4的例子中,4个组合分别是(1,1,1,1,1),(1,1,1,2),(1,2,2),(5),物品相对顺序都是1、2、5,所以只考虑一种排列顺序,求出来的就是组合数。

先背包的情况,是对于某个背包j,来看依次每个物品是否能装下,所以先看背包1能不能装下物品1、2、5,再看背包2能不能装下1、2、5,放入物品的这个顺序是1,2,5,1,2,5,1,2,5……,这里对于这三个数所有的排列(1,2,5;1,5,2;2,1,5;2,5,1;5,1,2,5,2,1)都考虑到了,所以求出来的是排列数而不是组合数。

所以这里只能先物品。

  1. class Solution {
  2. public:
  3. int change(int amount, vector<int>& coins) {
  4. vector<int> dp(amount+1,0);//[0,i]里面选,使得容量是j的背包 的硬币组合数
  5. dp[0]=1; //啥也不选也是一个组合
  6. for(int i=0;i<coins.size();++i){
  7. for(int j=coins[i];j<=amount;++j){ //背包是正序
  8. dp[j]+=dp[j-coins[i]];
  9. }
  10. }
  11. return dp[amount];
  12. }
  13. };

377. 组合总和 Ⅳ

说是组合总和,但是

顺序不同的序列被视作不同的组合。

所以其实是求排列数,刚好就是和上一题的 物品背包内外顺序 相反,其他都一样

  1. class Solution {
  2. public:
  3. int combinationSum4(vector<int>& nums, int target) {
  4. vector<int> dp(target+1,0);
  5. dp[0]=1;
  6. for(int j=0;j<=target;++j){
  7. for(int i=0;i<nums.size();++i){
  8. if(j>=nums[i] && dp[j]<INT_MAX-dp[j-nums[i]]){//dp[j]+dp[j-nums[i]]<INT_MAX
  9. dp[j]+=dp[j-nums[i]];
  10. }
  11. }
  12. }
  13. return dp[target];
  14. }
  15. };

会转换成01背包/完全背包 问题。

322. 零钱兑换

dp[j]的定义根据题目 是凑成j所需的 最少的硬币个数。

递推公式:还是两种情况,在第i个之前的最少硬币数(不选i);选上了i个的最少硬币数,这里应该取两者最小值。

遍历顺序:这里是求硬币个数而不是组合/排列 种数,所以先物品 或者先背包 都可以。但是是完全背包,所以物品、背包 遍历得从0开始。

初始化:有讲究,之前计算最大值,直接都初始化为0。这里是最小值。dp[0]肯定是=0。但是其他元素不能和之前一样初始化为0,想想之前最大价值初始化为0是为了不在取max的时候覆盖非0值,那么现在应该取INT_MAX,保证比过程中生成的元素值都大,就不会在取min的时候覆盖他们。保证元素能够被更新。

最后如果dp[amount]没有被更新,说明不能凑成amount,所以返回-1。

  1. class Solution {
  2. public:
  3. int coinChange(vector<int>& coins, int amount) {
  4. if(amount==0)return 0;
  5. vector<int> dp(amount+1,INT_MAX-1);
  6. dp[0]=0;
  7. for(int j=0;j<=amount;++j){
  8. for(int i=0;i<coins.size();++i){
  9. if(j>=coins[i] )dp[j]=min(dp[j-coins[i]]+1,dp[j]);
  10. }
  11. }
  12. if(dp[amount]==INT_MAX-1)return -1;
  13. return dp[amount];
  14. }
  15. };

322. 零钱兑换

跟上题一样计算最小值,物品其实是1到根号n这些数,他们的价值是i的平方。

  1. class Solution {
  2. public:
  3. int numSquares(int n) {
  4. //物品是1,2,3,……,根号n。
  5. vector<int> dp(n+1,INT_MAX-1);
  6. dp[0]=0;
  7. for(int i=1;i<=sqrt(n);++i){
  8. for(int j=i*i;j<=n;++j){
  9. dp[j]=min(dp[j],dp[j-i*i]+1);
  10. }
  11. }
  12. return dp[n];
  13. }
  14. };

139. 单词拆分

感觉说成是背包不太形象。

含义:dp[j]=true:可以利用字典中出现的一个或多个单词可以拼接出子串 [0,j]

普通的动态规划。内循环和外循环都是在字符串s上的。过程也是一维就能表示出来。

外循环i是1到n,意思是检查出子串[0,i]能不能被构成;要得出结论还得构造一个内循环j,因为可以把子串[0,i] 分成[0,j]和[j,i]两部分,[0,j]是dp[j]可以直接用,因为前面已经得到(体现出DP),[j,i]的话还需要自己来检查,是不是wordDict里面的某个单词(查找为了速度,使用Set结构)。找到了一个符合的j 就可以跳出内循环检查下一个子串[o,i+1] 了。

注意初始化dp[0]也是true。遇到第一个符合的单词才能让dp[0]和查找[0,j]都为true。

  1. class Solution {
  2. public:
  3. bool wordBreak(string s, vector<string>& wordDict) {
  4. //物品:wordDict。
  5. //背包容量:s
  6. vector<bool> dp(s.size()+1,false);
  7. dp[0]=true;
  8. unordered_set<string> wordSet(wordDict.begin(),wordDict.end());
  9. for(int j=1;j<=s.size();++j){
  10. for(int i=0;i<=j;++i){
  11. string sub=s.substr(i,j-i);
  12. if(wordSet.find(sub)!=wordSet.end() && dp[i]){
  13. dp[j]=true;
  14. break;//可以
  15. }
  16. }
  17. }
  18. return dp[s.size()];
  19. }
  20. };

时间复杂度:O(n2)

空间O(n)

背包总结:

问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); ,对应题目如下:

问装满背包有几种方法:dp[j] += dp[j - nums[i]] ,对应题目如下:

问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); ,对应题目如下:

问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); ,对应题目如下:

遍历顺序:观察递推公式,确定遍历顺序。

动态规划:关于01背包问题,你该了解这些!

(opens new window)中我们讲解二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。

动态规划:关于01背包问题,你该了解这些!(滚动数组)

 (opens new window)中,我们讲解一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历。

完全背包:

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

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

相关题目如下:

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/1017509
推荐阅读
相关标签
  

闽ICP备14008679号