动态规划:01背包问题全解

01背包

一、前言

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

这是最标准的01背包问题,以至于我们有时看到了这个自然就会想到背包,甚至都不知道暴力的解法应该怎么解了。

这样其实是没有从底向上去思考,而是习惯性想到了背包,那么暴力的解法应该是怎么样的呢?

每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是O(n*n),这里的n表示物品数量。由于时间复杂度过高,我们才会想到用动态规划来解决这一系列问题


二、动态规划思想

1.确定dp数组及其下标含义

dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少

2.确定递推公式

让我们再回顾一下dp[i][j]的含义:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。由此我们可以根据一下两个方向来确定递推公式:

不放物品i:此时dp[i][j]=dp[i-1][j],我们的dp含义已经是最大价值总和了,此时如果不选物品i,那么在背包容量为j的情况下挑选前i件物品的最大价值就等于在背包容量为j的情况下挑选前i-1件物品

放物品i:此时dp[i][j]=dp[i-1][j-weight[i]]+value[i],选择物品i后背包容量为j,那么在没选物品i时,相当于以j-weight[i]这一背包容量在前i-1件物品中挑取的最大价值,并加上i的价值

综上所述,因为我们要做出选当前物品和不选当前物品中价值最大的那一步选择,所以最后确定的递推公式为:

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

3.初始化

首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为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
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
for (int j = weight[0]; j <= bagweight; j++) {
    dp[0][j] = value[0];
}

4.确定遍历顺序

遍历分为两种,一种是先遍历背包,后遍历物品,另外一种是先遍历物品,后遍历背包。在01背包中先遍历背包和先遍历物品都是可以的

先遍历物品,后遍历背包

// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
    for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
        if (j < weight[i]) dp[i][j] = dp[i - 1][j];
        else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
    }
}

 先遍历背包,后遍历物品

// weight数组的大小 就是物品个数
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
    for(int i = 1; i < weight.size(); i++) { // 遍历物品
        if (j < weight[i]) dp[i][j] = dp[i - 1][j];
        else dp[i][j] = max(dp[i - 1][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]) ,我们可以清晰的发现任何一个此时选区的i物品的状态都依赖且只依赖于选取i-1物品时的状态,因此我们可以给dp数组降维,改为一轮一轮刷新覆盖一维数组来做到相同的效果

for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}

为什么背包的遍历顺序反过来了呢,这个问题网上很少有帖子能把它真正讲明白:

我们只看dp[j]=dp[j-weight[i]]这一步,因为是这一轮大容量背包可能找上一轮小容量背包要答案,如果此时背包在从小到大遍历,你这一步大容量背包找的小容量背包不再是上一轮了,而是这一轮的小容量背包,因此我们需要从后往前遍历容量,确保你大容量背包找的小容量背包是上一轮未被修改过的


四、Leetcode实战

当然大多数此类问题会在我们刚刚讲的经典01背包基础上做一些变化,我们需要根据最基础的01背包理论将这些问题转化为我们能够熟悉的01背包,然后再解决它们

Leetcode.416 分割等和子集

方法一:

class Solution {
public:
    bool canPartition(vector<int>& nums) {
    //01背包问题转化思路:在0...i中选取nums中的元素,能否恰好装满容量为nums和一半的背包
    //dp数组及其含义:dp[i][j]-->前i里面挑,能否挑出和为j
    //递推公式:dp[i][j]=dp[i-1][j]|dp[i-1][j-nums[i]]
    //初始化:dp[i][0]=true dp[0][nums[0]]=true
    int total=0;
    for(int i=0;i<nums.size();i++){
    total+=nums[i];
    }
    if(total%2!=0)return false;//base case:总和为奇数直接返回
    int target=total/2;
    //创建dp数组
    vector<vector<bool>>dp(nums.size(),vector<bool>(target+1,false));
    //进行dp数组的初始化
    for(int i=0;i<nums.size();i++){
        dp[i][0]=true;
    }
    if(target>=nums[0])
    dp[0][nums[0]]=true;
    for(int i=1;i<nums.size();i++){
        for(int j=1;j<=target;j++){
            if(j<nums[i])dp[i][j]=dp[i-1][j];
            else dp[i][j]=dp[i-1][j]|dp[i-1][j-nums[i]];
        }
    }
    return dp[nums.size()-1][target];

    }
};

滚动数组优化:

class Solution {
public:
    bool canPartition(vector<int>& nums) {
    int total=0;
    for(int i=0;i<nums.size();i++){
    total+=nums[i];
    }
    if(total%2!=0)return false;
    int target=total/2;
    vector<int>dp(target+1,0);
    for(int i=0;i<nums.size();i++){
        for(int j=target;j>=nums[i];j--){
            dp[j]=max(dp[j],dp[j-nums[i]]+nums[i]);
            if(dp[target]==target)return true;
        }
    }
    return false;

    }
};

方法二:

class Solution {
public:
    bool canPartition(vector<int>& nums) {
    //01背包问题转化思路:在0...i中选取nums中的元素,能否恰好装满容量为nums和一半的背包
    //dp数组及其含义:dp[i][j]-->前i里面挑,容量为j的背包最大价值(每件物品的价值等于其重量)
    //递推公式:dp[i][j]=max(dp[i-1][j],dp[i-1][j-nums[i]]+nums[i])
    //初始化:dp[0][nums[0]...target]=nums[0]
    int total=0;
    for(int i=0;i<nums.size();i++){
    total+=nums[i];
    }
    if(total%2!=0)return false;//base case:总和为奇数直接返回
    int target=total/2;
    //创建dp数组
    vector<vector<int>>dp(nums.size(),vector<int>(target+1,0));
    //进行dp数组的初始化
    for(int i=nums[0];i<=target;i++){
        dp[0][i]=nums[0];
    }
    for(int i=1;i<nums.size();i++){
        for(int j=1;j<=target;j++){
            if(j<nums[i])dp[i][j]=dp[i-1][j];
            else dp[i][j]=max(dp[i-1][j],dp[i-1][j-nums[i]]+nums[i]);
        }
    }
    return dp[nums.size()-1][target]==target;

    }
};

 滚动数组优化:

class Solution {
public:
    bool canPartition(vector<int>& nums) {
    int total=0;
    for(int i=0;i<nums.size();i++){
    total+=nums[i];
    }
    if(total%2!=0)return false;
    int target=total/2;
    //创建dp数组
    vector<bool>dp(target+1,false);
    dp[0]=true;
    //进行dp数组的初始化
    for(int i=0;i<nums.size();i++){
        for(int j=target;j>=nums[i];j--){
         dp[j]=dp[j]|dp[j-nums[i]];
         if(dp[target])return true;
        }
    }
    return false;

    }
};

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

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
    int total=0;
    for(int val:stones){
        total+=val;
    }
    int target=total/2;
    vector<vector<int>>dp(stones.size(),vector<int>(target+1,0));
    //容量为target的背包最多能装多少价值的物品,价值就正好等于重量
    for(int i=stones[0];i<=target;i++){
        dp[0][i]=stones[0];
    }
    for(int i=1;i<stones.size();i++){
        for(int j=1;j<=target;j++){
            if(j>=stones[i])dp[i][j]=max(dp[i-1][j],dp[i-1][j-stones[i]]+stones[i]);
            else dp[i][j]=dp[i-1][j];
        }
    }
    return total-2*dp[stones.size()-1][target];

    }
};

滚动数组优化:

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
    int total=0;
    for(int val:stones){
        total+=val;
    }
    int target=total/2;
    vector<int>dp(target+1,0);
    for(int i=0;i<stones.size();i++){
        for(int j=target;j>=stones[i];j--){
            dp[j]=max(dp[j],dp[j-stones[i]]+stones[i]);
        }
    }
    return total-2*dp[target];

    }
};

Leetcode.494 目标和

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
    //设加法总数为x,那么减法总数为sum-x,那么target需要等于2*x-sum,x=target+sum/2
    int sum=0;
    for(int val:nums){
    sum+=val;
    }
    if(sum<abs(target))return 0;
    if((sum+target)%2!=0)return 0;
    target=(target+sum)/2;
    vector<vector<int>>dp(nums.size(),vector<int>(target+1,0));
    //背包能够装出价值为x的个数
    //递推公式:dp[i][j]=max(dp[i-1][j],dp[i-1][j-nums[i]]+1)
    if(nums[0]<=target)dp[0][nums[0]]=1;
    if(nums[0]==0)dp[0][0]=2;
    else dp[0][0]=1;
    for(int i=1;i<nums.size();i++){
        if(nums[i]==0)dp[i][0]=2*dp[i-1][0];
        else dp[i][0]=dp[i-1][0];
    }
    for(int i=1;i<nums.size();i++){
        for(int j=1;j<=target;j++){
            if(j>=nums[i])dp[i][j]=dp[i-1][j]+dp[i-1][j-nums[i]];
            else dp[i][j]=dp[i-1][j];
        }
    }
    return dp[nums.size()-1][target];

    }
};

 滚动数组优化:

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
    int sum=0;
    for(int val:nums){
        sum+=val;
    }
    if((sum+target)%2!=0||sum<abs(target))return false;
    vector<int>dp((sum+target)/2+1,0);
    dp[0]=1;
    for(int i=0;i<nums.size();i++){
        for(int j=dp.size()-1;j>=nums[i];j--){
            dp[j]+=dp[j-nums[i]];
        }
    }
    return dp[dp.size()-1];
    }
};

Leetcode.474 一和零

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
    //dp[i][j][k]--->挑选前i个字符串,所选出来有j个0和k个1的最大子集
    //dp[i][j][k]=max(dp[i-1][j][k],dp[i-1][j-zeroNum][k-oneNum]+1)
    //初始化:将下标为0的字符串中的0和1统计一下,dp[0][i>=zeroNum][j>=oneNum]=1
    vector<vector<vector<int>>>dp(strs.size(),vector<vector<int>>(m+1,vector<int>(n+1,0)));
    int zeroNum=0;
    int oneNum=0;
    for(char c:strs[0]){
        if(c=='0')zeroNum++;
        else oneNum++;
    }
    for(int i=zeroNum;i<=m;i++){
        for(int j=oneNum;j<=n;j++){
            dp[0][i][j]=1;
        }
    }
    for(int i=1;i<strs.size();i++){
        zeroNum=0;
        oneNum=0;
        for(char c:strs[i]){
            if(c=='0')zeroNum++;
            else oneNum++;
        }
        for(int j=0;j<=m;j++){
            for(int k=0;k<=n;k++){
                if(j>=zeroNum&&k>=oneNum)
                dp[i][j][k]=max(dp[i-1][j][k],dp[i-1][j-zeroNum][k-oneNum]+1);
                else dp[i][j][k]=dp[i-1][j][k];
            }
        }
    }
    return dp[strs.size()-1][m][n];
    }
};

省去初始化的代码:外围数组扩一圈在for循环中进行初始化,注意,该方法仅仅是使代码更加简洁,实际上不会减少时间,反而会增加空间

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {        
    vector<vector<vector<int>>>dp(strs.size()+1,vector<vector<int>>(m+1,vector<int>(n+1,0)));
    
    for(int i=1;i<=strs.size();i++){
       int zeroNum=0;
       int oneNum=0;
        for(char c:strs[i-1]){
            if(c=='0')zeroNum++;
            else oneNum++;
        }
        for(int j=0;j<=m;j++){
            for(int k=0;k<=n;k++){
                if(j>=zeroNum&&k>=oneNum)
                dp[i][j][k]=max(dp[i-1][j][k],dp[i-1][j-zeroNum][k-oneNum]+1);
                else dp[i][j][k]=dp[i-1][j][k];
            }
        }
    }
    return dp[strs.size()][m][n];
    }
};

滚动数组优化:

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        vector<vector<int>>dp(m+1,vector<int>(n+1,0));
        for(string str:strs){
            int zeroNum=0;
            int oneNum=0;
            for(char a:str){
                if(a=='0')zeroNum++;
                else oneNum++;
            }
            for(int i=m;i>=zeroNum;i--){
                for(int j=n;j>=oneNum;j--){
                    dp[i][j]=max(dp[i][j],dp[i-zeroNum][j-oneNum]+1);
                }
            }
        }
        return dp[m][n];
    }
};

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值