动态规划系列---不再为【背包】问题烦恼

本篇文章将站在labuladong文章上对【背包】系列问题进行总结。

通过本文,你可以解决一下leetcode问题。

leetcode 416 分割等和子集

leetcode 518 零钱兑换 ||

动态规划帮助我们有效率地遍历问题的解,反复强调的在做动态规划时,需要注意的点有。

第一,明确【状态】和【选择】
第二,明确dp数组的含义
第三,找出状态转移方程式
第四,确认好边界(base line)情况

一般来说,对于我们比较困难在于第二和第三点,因为两者有密切的练习,我们需要明确含义才能想明白状态之间的改变。

labuladong给出的框架如下:
在这里插入图片描述
接下来我们将带着这个框架去解决【背包】系列问题。

0-1背包问题

我们都知道最经典的0-1背包是这样设定的:

给你一个可装载重量为W的背包和N个物品,每个物品有重量和价值两个属性。其中第i个物品的重量为wt[i],价值为val[i],现在让你用这个背包装物品,最多能装的价值是多少?
N = 3, W = 4
wt = [2, 1, 3]
val = [4, 2, 3]

既然是0-1背包,也就是说物品要么装,要么不装,不可以被分割。

接下来,我们套用标准模板来走一遍思路。

第一步,明确【状态】和【选择】。

【状态】对于我们描述问题是关键,也就是通过状态我们可以有效地表达出当前的问题情况。那么在背包问题中,【背包的容量】和【可选择的物品】就构成了我们的【背包问题】。

对于一个物品我们所拥有的【选择】显而易见,那就是**“放”**还不是“**不放”**这个物品。

第二步 明确dp数组的含义

在上一步中我们只是把关键的要素确定了出来,而在这一步中dp数组起到的就是描述问题的纽带。把我们在步骤一中提到的要素放在dp中,就可以有效地【描述问题】,所以是dp【】={所有状态的总和}。

第三步 根据选择明确状态的转移

对于选择来讲只有两种,放还是不放。那么我们还要对应写出,当物品选择“放”时的【状态】,和不放的状态。

相信大家已经都很熟悉了。

如果你没有把这第i个物品装入背包,那么很显然,最大价值dp[i][w]应该等于dp[i-1][w]。你不装嘛,那就继承之前的结果。

如果你把这第i个物品装入了背包,那么dp[i][w]应该等于dp[i-1][w-wt[i-1]] + val[i-1]。

第四步 最后不要忘记确认base line。

针对问题,考虑周全它的基本情况。其实在这里我们又可以加深一层体会,在搜寻问题所有解的时候。不管是回溯还是动态规划,本质上也是要找到问题的【突破口】。对于回溯算法,遍历到深处回退,以及动态规划从baseline上开始计算,都需要描述清楚。

附加步骤 状态压缩

当问题的状态比较多时,实际上动态规划求解的过程只需要依靠【上一个状态】做出抉择。基于此,我们常常可以把空间继续压缩,来优化算法,尽管这样会降低代码的可读性。

最后给出0-1背包的代码,懒得打了,来自labuladong,推广好文。

int knapsack(int W, int N, vector<int>& wt, vector<int>& val) {
    // vector 全填入 0,base case 已初始化
    vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
    for (int i = 1; i <= N; i++) {
        for (int w = 1; w <= W; w++) {
            if (w - wt[i-1] < 0) {
                // 当前背包容量装不下,只能选择不装入背包
                dp[i][w] = dp[i - 1][w];
            } else {
                // 装入或者不装入背包,择优
                dp[i][w] = max(dp[i - 1][w - wt[i-1]] + val[i-1], 
                               dp[i - 1][w]);
            }
        }
    }

    return dp[N][W];
}

0-1背包变体

实际上【背包】问题和子集划分的问题可以相互联系,我们来看实例。

在这里插入图片描述
乍一看可能还没那么快反应过来。实际上要求划分的两个子集元素和相等,假设该集合的所有元素和为sum,那么我们就得在集合中找到合适的元素,使得他们的和为sum/2。问题用【背包】语言翻译一下也就是:

给一个背包容量为【sum/2】和【N】个物品(元素),每个物品重量为 nums[i], 是否存在一种装法, 能将背包装满。

根据之前提出的框架和结构,写出代码。

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

       for(int i = 1;i<=n;i++){
           for(int j =0;j<=target;j++){
               if(j<nums[i-1]) dp[i][j]=dp[i-1][j];
               else dp[i][j]=dp[i-1][j]||dp[i-1][j-nums[i-1]];
           }
       }
       return dp[n-1][target];
    }
};

完全背包问题

在这里插入图片描述

完全背包的改变在于物品的使用是不限制的,因此状态的总和来自于【选】与【不选】状态的相加,即是上一个物体不管【选】还是【没选】都不会影响我当前的问题状态,隐藏的含义就是物体的使用是不限制的。

根据框架和步骤,给出代码。

class Solution {
public:
    int change(int amount, vector<int>& coins) {
     //base line
     vector<vector<int>> dp(coins.size()+1,vector<int>(amount+1,0));
     dp[0][0]=1;
      for(int i = 1;i<=coins.size();i++){
          for(int j=0;j<=amount;j++){
            if(j==0) dp[i][0] = 1;
            //if(coins[i]<j) dp[i][j] = 0;
            if(j>=coins[i-1]) dp[i][j] = dp[i-1][j]+dp[i][j-coins[i-1]];
            else dp[i][j]=dp[i-1][j]; 
          }
      }
    
    return dp[coins.size()][amount];

    }
};

写在最后

参考: https://mp.weixin.qq.com/s/zGJZpsGVMlk-Vc2PEY4RPw

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值