代码随想录算法训练营第四十二天| 二维背包、一维背包、LeetCode 416.分割等和子集

一、二维背包

文章讲解/视频讲解:https://programmercarl.com/%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.html

状态:已解决

1.背包问题介绍

        背包问题实则是一类问题的集合,有好多不同小类型,但本质还是一样的,基本组成都是:一个容量固定的背包、若干件物品(拥有体积、价值、数量三种属性),根据物品的数量的限制,可以将背包问题划分为以下几类:

        背包问题是动态规划的经典问题,也是难点所在,一般来说,学会01背包和完全背包就够做题了,再往上就是竞赛级别的了。而完全背包实则也是在01背包的基础上进行的变化升级,因此,01背包是重中之重,我们必须掌握。

2.01背包问题

        标准题意:有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

         这种取或不取的问题,我们自然容易想到用回溯法遍历所有情况暴力去做,但这种方法时间复杂度太高(指数级),在有更优化的解法时不建议使用。

        这里说的更优化的解法实则就是动态规划。具体怎么做我们利用动规五部曲来分析。

(1)确定dp数组以及下标含义:

        我们知道动规中dp数组是用于表示状态的一个数组,那我们这道题有哪些状态呢?首先,我们需要一个状态说明此刻该抉择选取哪个物品了,另外我们需要一个状态来说明此时背包的容量。那么,我们就可以定义一个二维数组dp。dp[i][j] 表示从下标为[0-i]的物品里任意取(0 <= k <=i,可能取了物品k也可能没取物品k),放到容量为j的背包,价值总和最大是多少。画出dp数组如图:

(2)确定递推公式:

        我们知道了dp[i][j]的含义:抉择第i个物品时,背包重量为j,那么结合逻辑,我们就可以得出状态dp[i][j]的来源:

  • 不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。)
  • 放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么在容量为 j 时可以选择多放入物品 i ,dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值。

        因此,递推公式为:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

(3)dp数组初始化:

        首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0(放不下任何东西)。如图:

         状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。那么很明显当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。当j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。代码初始化如下:

        其余下标由于dp[i][j]都是由左上角或者上面推导过来的,因此初始化就无所谓,反正都会被覆盖,为了方便,统一初始化为0。

(4)遍历顺序:

        我们知道dp是一个二维数组,也就是有两个维度:物品和背包容量,那么我们在进行循环的时候哪个维度放外层哪个维度放内层呢?我们根据递推公式可以知道一个元素是从左上和上面推出来的,也就是说,我们将物品作为外层,背包容量作为内层的话,那么从小到大遍历dp数组就是一排一排填满的,如果将背包容量作为外层,物品作为内层的话,那么dp数组就是一列一列的填满的。二者都可以顺利计算完整个dp数组(只要每排或每列是从小到大遍历的,那么我们做下一排或者下一列时,无论如何都能得到上面一格或者左上一格的元素)。

(5)举例推导dp数组:

        按现在的逻辑得到dp数组如下:

        最终结果就是dp[2][4]。 

3.具体实现 

        卡码网46题就是一个经典的01背包问题,我们根据上述分析可以给出代码:

#include<iostream>
#include<vector>
using namespace std;
int main(void){
    int m,n;
    cin>>m>>n;
    vector<int> weight(m,0);
    vector<int> value(m,0);
    vector<vector<int>> dp(m,vector<int>(n+1,0));
    for(int i=0;i<m;i++){
        cin>>weight[i];
    }
    for(int i=0;i<m;i++){
        cin>>value[i];
    }
    for(int i=weight[0];i<=n;i++){
        dp[0][i] = value[0];
    }
    for(int i=1;i<m;i++){
        for(int j=0;j<=n;j++){
            if(j-weight[i]>=0)
            	dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
            else
            	dp[i][j] = dp[i-1][j];
        }
    }
//    for(int i=1;i<m;i++){
//        for(int j=0;j<=n;j++){
//            cout<<dp[i][j]<<" ";
//        }
//        cout<<endl;
//    }
    cout<<dp[m-1][n];
}

二、一维背包 

文章讲解/视频讲解:https://programmercarl.com/%E8%83%8C%E5%8C%85%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%8001%E8%83%8C%E5%8C%85-2.html

状态:已解决

1.滚动数组 

        一维背包实则还是01背包问题,只是我们将状态数组从二维降到了一维。依据是什么呢?我们根据二维背包的分析可以得知二维dp数组的每一层的每一个格子(除初始化的最左侧和最上方)实则都是从上一层的左上角格子和正上方格子推导出来的,也就是说,每一层的值只跟上一层的值有关,跟这层的值没有关系。也就是说,我们只需要保存上一层的状态,然后推导出这一层的状态后,舍弃上一层状态,更新为新推导出的这一层状态,由此再去推导下一层。即,整个二维数组被压缩成一行(层),然后不断地向下滚动着更新,故称之为滚动数组。由于数组降维,从两个状态变为了一个状态,故这种优化方法也被称为状态压缩。

(1)确定dp数组及下标含义:

在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。

(2)一维dp数组的递推公式:       

  我们知道二维dp数组的递推公式是:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]),i是物品标号也是层数标号,当层数被压缩为一层时,我们知道当前一维数组实际就是推导完的上层状态,那么原先上层的dp[i - 1][j], dp[i - 1][j - weight[i]],实际就是现在的dp[j]、dp[j-weight[i]]。故递推公式为:

dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

         含义:dp[j - weight[i]] + value[i] 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j])

        此时dp[j]有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i。

(3)dp数组的初始化:

        dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该取0,因为容量为0的书包装不下任何东西,故所背的物品的最大价值就是0。        

        那么dp数组除了下标0的位置,初始为0,其他下标应该初始化多少呢?看一下递归公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了

(4)一维dp数组的遍历顺序:       

        假如现在外层是依次遍历物品。那么,内层的背包容量也还是从小到大来遍历吗?漏!!!我们知道,内层循环现在是背包容量,由于现在的dp[ j ]是由dp[j]和dp[j-weight[i]]决定的,那么我们推导dp[j]时,本身需要的是上一层在 j 和 j-weight[i]时的值,而如果每次对于物品i,都从小到达遍历容量的话,那么在算这层的 j容量 之前时,它前面的容量值((比如j - weight[i]))就已经被更新成这层的推算值,而不是上层的推算值了。因此,我们只会反复根据第一层物品的价值和重量进行推导,而不是这层物品的相关值。

        例:物品0的重量weight[0] = 1,价值value[0] = 15。如果正序遍历

        dp[1] = dp[1 - weight[0]] + value[0] = 15

        dp[2] = dp[2 - weight[0]] + value[0] = 30

此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。

        为什么倒序遍历,就可以保证物品只放入一次呢?因为倒序从当层的末尾容量开始推导,那么只动了j容量后面的值,j前面的值仍然为上层的推算值,由此推得的dp[j] 也就是由上层推算值推导出来的正确的dp[j]。倒序就是先算dp[2]dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)dp[1] = dp[1 - weight[0]] + value[0] = 15。所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。

       再来看看两个嵌套for循环的顺序,代码中是先遍历物品嵌套遍历背包容量,那可不可以先遍历背包容量嵌套遍历物品呢?不可以!因为一维dp的写法,背包容量一定是要倒序遍历(原因上面已经讲了),如果遍历背包容量放在上一层,那就是先计算最后一列的数值,而我们知道某个格子的数值是依赖左上角的值的,但此时左上角(倒数第二列)还没计算,故推不出正确结果,因此只能是物品先遍历

        倒序遍历的原因是,本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。

(5)举例推导dp数组:

2.代码实现 

        还是以卡码网46题为例。

#include<iostream>
#include<vector>
using namespace std;
int main(void){
    int m,n;
    cin>>m>>n;
    vector<int> weight(m,0);
    vector<int> value(m,0);
    vector<int> dp(n+1,0);
    for(int i=0;i<m;i++){
        cin>>weight[i];
    }
    for(int i=0;i<m;i++){
        cin>>value[i];
    }
    for(int i=0;i<m;i++){
        for(int j=n;j>=0;j--){
            if(j-weight[i]>=0)
            	dp[j] = max(dp[j],dp[j-weight[i]]+value[i]);
            else
            	dp[j] = dp[j];
        }
    }
//    for(int i=1;i<m;i++){
//        for(int j=0;j<=n;j++){
//            cout<<dp[i][j]<<" ";
//        }
//        cout<<endl;
//    }
    cout<<dp[n];
}

三、LeetCode 416.分割等和子集

题目链接/文章讲解/视频讲解:https://programmercarl.com/0416.%E5%88%86%E5%89%B2%E7%AD%89%E5%92%8C%E5%AD%90%E9%9B%86.html

状态:已解决

1.思路 

        这道题不难,关键在于如何将问题转换为01背包问题。题眼:将这个数组分割成两个子集,使得两个子集的元素和相等。我们知道,给定了数组,那么数组的和sum就是确定的,也就是说,我们现在的目标就是找一个集合使得集合元素之和等于sum/2,那么剩余元素构成的集合的和也就为sum/2了。现假设数组元素个数为m,那么套用到背包模型中,就是将m个物品(重量、价值均为nums[i]),装到容量为sum/2的背包中去,看是否能够装满背包。

        那么怎么判断是否能够装满背包呢?看背包最多能装的总价值是否刚好等于sum/2,即dp[target] == target。理由:

        (1)dp[target] > target 的情况不可能出现,因为现在一个物品的价值等于这个物品的重量,装满一个背包最多价值等于重量,不可能出现最终价值超过该背包容量的情况。

        (2)dp[target] < target:说明尽量装背包装不到tagret,也就是装不满背包,故不可能凑到某个集合的元素之和等于sum/2。

2.代码实现

        直接在上面的代码上做修改就行:m = num.size(),n = sum/2,weight[i] = nums[i],value[i] = nums[i]。

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;
        for(int i=0;i<nums.size();i++){
            sum += nums[i];
        }
        int m = nums.size(),n = sum/2;
        if(sum % 2 != 0) return false;//此处可剪枝
        vector<int> dp(n+1,0);
        for(int i=0;i<m;i++){
            for(int j=n;j>=0;j--){
                if(j-nums[i]>=0)
                    dp[j] = max(dp[j],dp[j-nums[i]]+nums[i]);
                else
                    dp[j] = dp[j];
            }
        }
        if(dp[n] == n) return true;
        else return false;
    }
};

时间复杂度:O(n^2)

空间复杂度:O(n)

  • 18
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值