01背包问题

背包问题

源自代码随想录动态规划:关于01背包问题,你该了解这些!

01 背包

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

重量价值
物品0115
物品1320
物品2430
/**
 *
 * @param {*} weight 物品重量
 * @param {*} value 物品的价值
 * @param {*} size 背包的种类
 * @returns
 */
function knapsack(weight, value, size) {

}

1、回溯法解决 0-1 背包问题

回溯法其实就是一个穷举的过程,将所有可能的情况进行穷举,从中寻找最优解,也就是本题的能装入背包的最大价值。

/**
 *
 * @param {*} weight 物品重量
 * @param {*} value 物品的价值
 * @param {*} size 背包的种类
 * @returns
 */
function knapsack(weight, value, size) {
  // 1. 回溯求解 0 - 1 背包问题
  let maxValue = Number.MIN_VALUE;

  const backTrack = (i, total, totalValue) => {
    // 背包装满了 或者 物品都考察完了
    if (total === size || i === weight.length) {
      if (totalValue > maxValue) {
        maxValue = totalValue;
      }
      return;
    }

    // 选择不装物品 i
    backTrack(i + 1, total, totalValue);

    if (total + weight[i] <= size) {
      // 选择装物品 i
      backTrack(i + 1, total + weight[i], totalValue + value[i]);
    }
  };

  backTrack(0, 0, 0);

  return maxValue;
}

knapsack([1, 3, 4], [15, 20, 30], 4); // 35

时间复杂度:O(2^n),n 为物品的件数

可以看到回溯算法的时间复杂度是指数级别的,相当高,下面使用动态规划求解一下!

2、动态规划解决 0-1 背包问题

使用动态规划五部曲:

  1. 确定dp数组(dp table)以及下标的含义

dp[i][j]: 表示物品从0 - i 之间任意选取装入容量为 j 的背包的最大价值

理解 dp 数组的含义非常重要,不要一直思考要往背包中装多少物品才能把背包装满,然后求最大值。这里使用动态规划的思想是枚举所有可能的状态,横向是物品,纵向是容量

  1. 确定递推公式

    对于物品 i 只有两种状态,选择装入背包或者不选择装入背包

    不选择装入背包

    如果不选择则和上一个状态保持一致,即

    dp[i][j] = dp[i - 1][j];
    

    选择装入背包

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

    dp[i - 1][j - weight[i]] 为背包容量为 j - weight[i] 时不放入物品 i 的最大价值,那么 dp[i - 1][j - weight[i]] + value[i] 就是背包容量为 j - weight[i] 时放入物品 i 的最大价值。

    由于数组索引从 0 开始,而我们定义中的 i 是从 1 开始计数的,所以 value[i-1]weight[i-1] 表示第 i 个物品的价值和重量。

    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]);
    
  2. dp数组如何初始化

    这里在创建 dp 时将数组所有元素先初始化为 0,后续在 迭代过程中物品从 i = 1 开始

  3. 确定遍历顺序

    遍历物品或者遍历背包容量都可以,因为这里的求值是根据 左上角某个元素进行迭代的,遍历顺序不影响结果

  4. 举例推导dp数组

    console.table(dp);

/**
 *
 * @param {*} weight 物品重量
 * @param {*} value 物品的价值
 * @param {*} size 背包的种类
 * @returns
 */
function knapsack(weight, value, size) {
  const length = weight.length;
  let dp = new Array(length + 1).fill().map(() => new Array(size + 1).fill(0));

  for (let i = 1; i <= length; i++) {
    for (let j = 0; j <= size; j++) {
      if (weight[i - 1] <= j) {
        // 由于数组索引从 0 开始,而我们定义中的 i 是从 1 开始计数的,
				// 所以 value[i-1] 和 weight[i-1] 表示第 i 个物品的价值和重量。
        dp[i][j] = Math.max(
          dp[i - 1][j],
          dp[i - 1][j - weight[i - 1]] + value[i - 1]
        );
      } else {
				// 物品的容量大于背包的容量,放不下
        dp[i][j] = dp[i - 1][j];
      }
    }
  }

  console.table(dp);

  return dp[length][size];
}

dp 数组打印如下:

┌─────────┬───┬────┬────┬────┬────┐
│ (index) │ 0 │ 1  │ 2  │ 3  │ 4  │
├─────────┼───┼────┼────┼────┼────┤
│    0    │ 0 │ 0  │ 0  │ 0  │ 0  │
│    1    │ 0 │ 15 │ 15 │ 15 │ 15 │
│    2    │ 0 │ 15 │ 15 │ 20 │ 35 │
│    3    │ 0 │ 15 │ 15 │ 20 │ 35 │
└─────────┴───┴────┴────┴────┴────┘

3、动态规划二维优化为一维(滚动数组)

使用动态规划五部曲:

  1. 确定dp数组(dp table)以及下标的含义

    dp[j]: 容量为 j 的背包,所背的物品价值可以最大为 dp[j]

  2. 确定递推公式

    dp[j - weight[i]] 表示容量为 j - weight[i] 的背包所背的最大价值。

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

    dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    
  3. 一维dp数组如何初始化

    dp[j] 表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。

  4. 一维 dp 数组遍历顺序

      // 遍历物品
      for (let i = 0; i < weight.length; i++) {
        // 再倒序遍历背包容量
        for (let j = size; j >= weight[i]; j--) {
          
        }
      }
    

    如果正序遍历

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

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

    为什么倒序遍历,就可以保证物品只放入一次呢?

    倒序就是先算dp[2]

    dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)
    
    dp[1] = dp[1 - weight[0]] + value[0] = 15
    

    所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。

    先遍历物品嵌套遍历背包容量,那可不可以先遍历背包容量嵌套遍历物品呢?

    不可以!

    因为一维dp的写法,背包容量一定是要倒序遍历(原因上面已经讲了),如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。

    (这里如果读不懂,就在回想一下dp[j]的定义,或者就把两个for循环顺序颠倒一下试试!)

    所以一维dp数组的背包在遍历顺序上和二维其实是有很大差异的!

/**
 *
 * @param {*} weight 物品重量
 * @param {*} value 物品的价值
 * @param {*} size 背包的种类
 * @returns
 */
function knapsack(weight, value, size) {
  let dp = new Array(size + 1).fill(0);

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

  return dp[size];
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值