lintcode:92 背包问题

题目来源

题目描述

在这里插入图片描述

题目解析

这是一个典型的背包问题:

  • 对于每个物品,可以选择装/不装(决策,因此可以选择动态规划),每个物品最大只能装一次
  • 要求:总重量不能超过m,求最大最装的重量

可以将每个物品的价值等价于它自己的重量

动态规划

思路

(1)确定状态

  • 最后一步:最后一个物品(重量为 A n − 1 A_{n-1} An1)是否装入背包(不要去想最终方案会不会装入背包,反正肯定需要遍历数组),假设放入当前物品之前的重量为 W n − 2 W_{n-2} Wn2
    • 情况一:不装入,重量不变,为 W n − 2 W_{n-2} Wn2
    • 情况二:装入,重量变为 W n − 2 + A n − 1 W_{n-2}+ A_{n-1} Wn2+An1
    • 取上面两种决策中重量的那个
    • 装入或者不装入取决于当前物品和剩余重量的关系
  • 子问题:
    • 要求前n个物品对于背包w能够获得的最大重量
    • 先要求前n-1个物品对于背包w能够获得的最大重量
  • 状态:
    • 上面有三个变化维度:第i个物品,背包容量j,能够获得的最大重量
    • 因此 d p [ i ] [ j ] dp[i][j] dp[i][j]表示前i个物品装入大小为j的背包中,可以获得的最大重量
      • d p [ a . s i z e ( ) ] [ i ] dp[a.size()][i] dp[a.size()][i]最后一个物品[0…a.size() - 1]
      • d p [ i ] [ j ] dp[i][j] dp[i][j]前i个物品 [ 0..... i − 1 ] [0.....i-1] [0.....i1]
      • dp[2][i] 前2个物品[0…1]
      • dp[1][j] 前1个物品[0…0]
      • dp[0][j] 前0个物品[0…-1]
      • 也就是说,dp的第一维长度为a.size() + 1,第二维长度为w+1(物品重量+1),结果为最大重量(int类型)
    • 注意这里是前 i 件物品,因此我们当前面对的物品下标是 i - 1,重量为A[i-1],注意,在所有背包问题中,都是用的这种 ”前 i 项“ ,而不是 ”第 i 项“ 来表示状态,这是因为 ”第 0 项“ 没法表示背包为空的情况。

(2)转移方程

  • 对于dp[i][j]表示前i个物品装入大小为j的背包中,可以获得的最大重量
  • 那么对于前i个物品的最后一个物品,也就是第i-1个物品,是否放入背包
    • 如果当前物品重量大于背包容量 A i − 1 > j A_{i-1} > j Ai1>j,只有一种决策
      • 不能放入,因此最大重量为 考 虑 前 i − 1 个 物 品 , 背 包 容 量 为 j 的 最 大 重 量 考虑前i-1个物品,背包容量为j的最大重量 i1j,也即是 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i - 1][j] dp[i][j]=dp[i1][j]
    • 如果当前物品重量小于等于背包容量 A i − 1 > j A_{i-1} > j Ai1>j,两种决策:
      • 不放入: d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i - 1][j] dp[i][j]=dp[i1][j]
      • 放入:由于背包已经占用了 A[i-1] 的重量,我们要找的就是在剩余容量下的最大装载量,也就是说: d p [ i ] [ j ] = d p [ i − 1 ] [ j − A i − 1 ] + A i − 1 dp[i][j] = dp[i-1][j - A_{i-1}] + A_{i-1} dp[i][j]=dp[i1][jAi1]+Ai1
      • 两种决策中选择一种价值最大的

(3)初始情况和边界条件

  • 初始情况:
    • dp[0][j] = 0 也就是没有物品时(前0个物品),获得的最大重量是0
    • dp[i][0] = 0 也就是背包容量为0时,能够获得的最大重量也是0

(4)遍历顺序

  • 两个维度,双重循环
  • 先初始化dp[0][j]为0
  • 然后从上到下[1…a.size() - 1]、从左到右[1…w]
  • 答案是dp[a.size()][w]:表示,对于数组个物品,装入w背包的能够获得的最大重量

(5)举个例子

  • arr = {3,4,8,5}, w = 10
    在这里插入图片描述
class Solution {
public:

    int backPack(int m, vector<int> &a) {
        int n = a.size();
        if(m == 0 || n == 0){
            return 0;
        }

        std::vector<std::vector<int >> dp(n +1, std::vector<int>(m + 1));
        for (int i = 0; i < n + 1; ++i) { // 前i个物品
            for (int j = 0; j < m + 1; ++j) {
                if(i == 0 || j == 0){
                    dp[i][j] = 0;  //没有物品 || 背包承重为0
                }else if(j - a[i - 1] >= 0){ // 放得下
                    dp[i][j] = std::max(
                            dp[i - 1][j],
                            dp[i - 1][j - a[i - 1]] + a[i - 1]
                            );
                }else{
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }


        return dp[n][m];
    }
};

class Solution {
public:

    int backPack(int m, vector<int> &a) {
        int n = a.size();
        if(m == 0 || n == 0){
            return 0;
        }

        std::vector<std::vector<int >> dp(2, std::vector<int>(m + 1));
        for (int i = 0; i < n + 1; ++i) { // 前i个物品
            for (int j = 0; j < m + 1; ++j) {
                if(i == 0 || j == 0){
                    dp[i % 2][j] = 0;  //没有物品 || 背包承重为0
                }else if(j - a[i - 1] >= 0){ // 放得下
                    dp[i % 2][j] = std::max(
                            dp[(i - 1) % 2][j],
                            dp[(i - 1) % 2][j - a[i - 1]] + a[i - 1]
                            );
                }else{
                    dp[i % 2][j] = dp[(i - 1) % 2][j];
                }
            }
        }


        return dp[n % 2][m];
    }
};


class Solution {
public:
    /**
     * @param m: An integer m denotes the size of a backpack
     * @param a: Given n items with size A[i]
     * @return: The maximum size
     */
    int backPack(int m, vector<int> &a) {
        int len = a.size();
 		if(m == 0 || len  == 0){
            return 0;
        }

        std::vector<std::vector<int>> dp(len + 1 , std::vector<int>(m + 1));

        for (int i = 1; i <= len; ++i) { // 当放第1个到第A.length-1个物品时
            for (int j = 1; j <= m; ++j) {  // 遍历重量
                if(j >= a[i - 1]){  
                //若该物品所占空间小于等于背包总空间,则需将背包空间腾出至少A[i - 1]后,将该物品放入。
                //放入新物品后背包最大可装满空间可能更大,也可能变小大,
                // 取大值作为背包空间为j且放第i个物品时可以有的最大可装满空间。
                    dp[i][j] =  std::max(
                            dp[i - 1][j - a[i - 1]] + a[i - 1],  // 放
                            dp[i - 1][j]);  // 不放
                }else{
                    dp[i][j] = dp[i - 1][j];  //背包可装满的最大空间不变
                }
            }
        }



        return dp[len][m];
    }
};

在这里插入图片描述

class Solution {
public:
    /**
     * @param m: An integer m denotes the size of a backpack
     * @param a: Given n items with size A[i]
     * @return: The maximum size
     */
    int backPack(int m, vector<int> &a) {
        // 如果背包容量或者物品数量为0,则直接返回
        int n = a.size();
        if(m == 0 || n == 0){
            return 0;
        }

        std::vector<std::vector<int>> dp(n , std::vector<int>(m + 1));
        // 初始化第一行
        for (int i = 0; i < m + 1; ++i) {
            // 枚举当前背包容量是否可以放下第一个物品
            dp[0][i] = i >= a[0] ? a[0] : 0;
        }

        
        // 之后的每一个物品都可以根据前一个物品的状态求出来
        for (int i = 1; i <= n; ++i) { //遍历每一个物品
            for (int j = 0; j <= m; ++j) {  // 遍历每一个重量
              int unsel = dp[i - 1][j]; // 当前物品不放入
              // 选择当前物品:前提是背包容量能放下这件物品
              int sel = j >= a[i] ? dp[i - 1][j - a[i]] + a[i] : 0;
              dp[i][j] = std::max(unsel, sel);
            }
        }



        return dp[n - 1][m];
    }
};

滚动数组优化:

class Solution {
public:
    /**
     * @param m: An integer m denotes the size of a backpack
     * @param a: Given n items with size A[i]
     * @return: The maximum size
     */
    int backPack(int m, vector<int> &a) {
        // 如果背包容量或者物品数量为0,则直接返回
        int n = a.size();
         if(m == 0 || n == 0){
            return 0;
        }

        std::vector<std::vector<int>> dp(2 , std::vector<int>(m + 1));
        // 初始化第一行
        for (int i = 0; i < m + 1; ++i) {
            // 枚举当前背包容量是否可以放下第一个物品
            dp[0][i] = i >= a[0] ? a[0] : 0;
        }


        // 之后的每一个物品都可以根据前一个物品的状态求出来
        for (int i = 1; i <= n; ++i) { //遍历每一个物品
            for (int j = 0; j <= m; ++j) {  // 遍历每一个重量
              int unsel = dp[(i - 1) & 1][j]; // 当前物品不放入
              // 选择当前物品:前提是背包容量能放下这件物品
              int sel = j >= a[i] ? dp[(i - 1) & 1][j - a[i]] + a[i] : 0;
              dp[i&1][j] = std::max(unsel, sel);
            }
        }



        return dp[(n - 1)&1][m];
    }
};

思路

  • 所有的背包问题,先从重量入手,已知一个背包的最大承重是 m m m,不管什么装,每个装物品的方案的总重量只能从 0 − m 0-m 0m中选择
  • 那我们看每一个重量能不能装满就可以了

(1)确定状态

  • 最后一步:前n个物品的最后一个物品(重量为 A n − 1 A_{n-1} An1)是否装入背包
    • 情况一:如果前 n − 1 n-1 n1个物品能够拼出 m m m,那么前 n n n个物品也能拼出 m m m
    • 情况二:如果前 n − 1 n-1 n1个物品能够拼出 m − A n − 1 m - A_{n-1} mAn1,那么最后加上物品 A n − 1 A_{n-1} An1,拼出 m m m
  • 子问题:
    • 要求前 n n n个物品能否拼出重量 0..... m 0.....m 0.....m
    • 先要求出前n-1个物品能否拼出 0.... m 0....m 0....m
  • 状态:
    • 上面有变化维度:第i个物品,重量j,能否拼出
    • dp[i][j]表示对于前i个物品,能否拼出重量j( t r u e / f a l s e true/false true/false)

(2)转移方程

  • 对于dp[i][j]
    • 不能装,那么取决于前i-1个物品能否拼出 j j j d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i - 1][j] dp[i][j]=dp[i1][j]
    • 能装( A i − 1 < = j A_{i-1} <= j Ai1<=j
      • 如果 A i − 1 = = j A_{i-1} == j Ai1==j,能拼: d p [ i ] [ j ] = t r u e dp[i][j] = true dp[i][j]=true
      • 如果 A i − 1 < j A_{i-1} < j Ai1<j,取决于前i-1个物品能否拼出 j − A i − 1 j - A_{i-1} jAi1 d p [ i ] [ j ] = d p [ i − 1 ] [ j − A i − 1 ] dp[i][j] = dp[i-1][j - A_{i-1}] dp[i][j]=dp[i1][jAi1]

(3)初始条件&&边界情况

  • 初始条件:
    • dp[0][0] = true,前0个物品也就是没有物品时,有一种方式能拼出重量0
    • dp[0][1…m]=false,0个物品不能拼出大于0的重量
    • dp[1…n][0] = true, 当有物品时,能拼出物品为0的方案
  • 边界情况:

(4)计算顺序

  • 从左到右
  • 从上到下
  • 答案是dp[i][j]右下角最后为true为那个j
class Solution {
public:

    int backPack(int m, vector<int> &a) {
        int n = a.size();
        if(m == 0 || n == 0){
            return 0;
        }

        std::vector<std::vector<bool >> dp(n +1, std::vector<bool>(m + 1));
        dp[0][0] = true;
        for (int i = 1; i < n + 1; ++i) { // 前i个物品
            dp[i][0] = true;
            for (int j = 1; j < m + 1; ++j) { //对于重量[1...m], 能否装满
                // 前i个物品
                if(a[i - 1] > j){   // 当物品重量 > 背包重量时
                    dp[i][j] = dp[i - 1][j];
                }else if(a[i - 1]  == j){
                    dp[i][j] = true;
                }else{
                    dp[i][j] =  dp[i - 1][j] or dp[i - 1][j - a[i - 1]];
                }
            }
        }

        int res = 0;
        for (int sum = m; sum >=0; --sum) {
            if (dp[n][sum]) {
                return sum;
            }
        }
        return 0;
    }
};

一维数组优化

class Solution {
public:
    /**
     * @param m: An integer m denotes the size of a backpack
     * @param a: Given n items with size A[i]
     * @return: The maximum size
     */
    int backPack(int m, vector<int> &a) {
        // 如果背包容量或者物品数量为0,则直接返回
        int n = a.size();
        if(m == 0 || n == 0){
            return 0;
        }

        // 对于每一个背包,能不能把它装满
        vector<int> dp(m+1,0);

        for (int i = 0; i < n; ++i) { // 对于每一个物品
            for (int j = m; j >= 0; --j) {  // 计算第二行时,因为会用到第一行前面的数据,因此从第一行尾端往前端进行覆盖
                int sel = dp[j];  // 放入
                int unsel = dp[j - a[i]] + a[i];  // 不放入
                dp[j] = max(sel, unsel);
            }
        }

        return dp[m];
    }
};

记忆化搜索

  • 如果要我们设计一个DFS函数对所有的方案进行枚举的话,大概是这么一个函数签名:
int dfs(int[] A, int idx, int letfM, int totalM);
  • 其中A和[输入的物品重量]、totalM表示[背包总重量],属于不变参数,而idx和letfM分别代表[当前枚举到哪件物品]和[现在的剩余容量]
  • 要求返回最大重量

递归三部曲:

  • 肯定需要枚举每一个物品,当枚举完所有的的物品(idx >= a.size())时,返回0
  • 对于每一个物品,有两种选择
    • 当[物品重量]大于[剩余容量],只有一种决策,那就是不放入
    • 当[物品重量]小于等于[剩余容量],那么有两种决策,放入 or 不放入,两种决策中选择一种重量较大的

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值