谈01背包和完全背包

背景

有 n 个物品和容量为 V 的背包,第 i 件物品的体积为 ci,价值为 wi。现在的目标是确定要将哪些物体放入背包,以保证在体积 不超过背包容量 的前提下,背包内的 总价值最高。

其次,基于以上前提条件,通过添加不同的约束条件,就形成了不同的背包问题。

01背包(二维数组)

约束条件:每种物品数量为 1,可以选择放或不放

状态转移方程

状态定义:f[i][v]为前 i 个 物品中,体积恰好为 v 时的最大价值。

result = max(f[n][0 ~ V])即最终答案,它表示前 n个物品的最大价值,假设这时容量为 k ,由于 0 <= k <= V,因此容量要在 0 ~ V求最大值来寻找 k。

状态转移方程:

javascript 复制代码
f[i][v] = max(f[i-1][v], f[i-1][v-c[i]] + w[i])

以上得到的状态 i 和状态 i-1 关系是从 实际意义 推断出来的。

  • 如果 不选第 i 个物品,那么前 i 个背包的最大价值就是前 i-1 个物品的价值,即 fiv = fi-1v;
  • 如果 选择了第 i 个物品,前 i-1 个物品的体积就是 v-ci,状态方程为 fi-1v-c\[i],注意这时的价值是前 i-1 个物品的价值,因此少了 wi 的价值,所以 fiv = fi-1v-c\[i] + wi。

最后我们要在这两种情况中选择能使物品最大价值更大的那个方案,故取最大值。最终我们就得到了以上的状态转移方程。

代码实现

由于 f[i][v] 是二维的,所以我们可以使用 二维数组 实现,时间复杂度和空间复杂度均为 O(nV)。

javascript 复制代码
let f = new Array(n+1).fill(0).map(() => new Array(V+1).fill(0)); // 初始化二维数组全为零

for (let i = 1; i <= n; i++) { // 枚举前i个物品
  for (let v = 0; v <= V; v++) { // 枚举体积
    f[i][v] = f[i-1][v]; // 不选择第i个物品
    if (v >= c[i]) { // 第i个物品的体积必须小于v才能选择
      f[i][v] = Math.max(f[i][v], f[i-1][v-c[i]] + w[i]);
    }
  }
}

let result = Math.max(...f[n]); // 返回前n个物品的最大值
return result;

二维数组又可以优化成如下

javascript 复制代码
let f = new Array(n+1).fill(0).map(() => new Array(V+1).fill(0)); // 初始化二维数组全为零

for (let i = 1; i <= n; i++) { // 枚举前i个物品
  for (let v = c[i]; v <= V; v++) { // 枚举体积
      f[i][v] = Math.max(f[i-1][v], f[i-1][v-c[i]] + w[i]);
  }
}

let result = Math.max(...f[n]); // 返回前n个物品的最大值
return result;

我们甚至可以把i给优化掉。

为什么?

先看看fiv的定义:f[i][v]为前 i 个 物品中,体积恰好为 v 时的最大价值。

我们需要的信息是,所有物品中,背包体积为v的最大值。即fMAX_COUNTv。其中MAX_COUNT为常量,v需要参与动态规划,所以是变动的。

完全背包问题

那么就使用一维数组来实现,但是可使空间复杂度降到 O(V)。

javascript 复制代码
let f = new Array(n+1).fill(0).map(() => new Array(V+1).fill(0)); // 初始化二维数组全为零

for (let i = 1; i <= n; i++) { // 枚举前i个物品
  for (let v = c[i]; v <= V; v++) { // 枚举体积
      f[v] = Math.max(f[v], f[v-c[i]] + w[i]);
  }
}

return f(n)

这段代码看上去ok吗?

其实不ok,因为状态表达式 f[i][v] = Math.max(f[v], f[v-c[i]] + w[i])中,fv的值需要用上一轮的fv-c\[i]的值去更新。

为什么?

因为上一轮迭代记录了i-1个物品在背包不同的体积下的状态,一个物品只能放一次。如果我们使用正序,会出现重复放物品的情况。

让我们举个反例看看:

  • 初始状态:

    • f = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
  • i = 1, 第1个物品(体积2,价值3):

    • f = 0, 0, 3, 3, 6, 6, 9, 9, 12, 12, 15
  • i = 2,第2个物品(体积3,价值4):

    • f = 0, 0, 3, 4, 6, 7, 9, 10, 12, 13, 15
  • i = 3, 加入第3个物品(体积4,价值5):

    • f = 0, 0, 3, 4, 6, 7, 9, 10, 12, 13, 15
  • i = 4, 加入第4个物品(体积5,价值6):
    • f = 0, 0, 3, 4, 6, 7, 9, 10, 12, 13, 15

可以看到,当一轮循环中背包体积变大时,物品会被重复放置。

这个反例其实就是完全背包。

01背包(一维数组)

为了避免重复放置的情况,所以内部循环需要倒序。

javascript 复制代码
let f = new Array(V+1).fill(0); // 初始化一维数组全为零

for (let i = 1; i <= n; i++) { // 枚举前i个物品
  for (let v = V; v >= c[i]; v--) { // 注意这里是倒序遍历
    f[v] = Math.max(f[v], f[v-c[i]] + w[i]);
  }
}
return f(n)

我们试着用一个例子来理解这个一维数组

假设我们有一个背包,其容量为 V为10。我们有4个物品,它们的体积和价值分别如下:

物品: 1 2 3 4 体积: c = 2, 3, 4, 5 价值: w = 3, 4, 5, 6

我们逐步分析代码执行结果:

  1. 初始状态:
    • f = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
  2. i = 1, 第1个物品(体积2,价值3):
    • f = 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3
  3. i = 2, 第2个物品(体积3,价值4):
    • f = 0, 0, 3, 4, 4, 7, 7, 7, 7, 7, 7
  4. i = 3, 第3个物品(体积4,价值5):
    • f = 0, 0, 3, 4, 5, 7, 8, 9, 9, 12, 12
  5. i = 4, 第4个物品(体积5,价值6):
    • f = 0, 0, 3, 4, 5, 7, 8, 9, 10, 12, 13

可以看到:

  • 可以看到,对于第一个物品,只要背包体积够,就会被放入
  • 对于第二个物品,则会根据状态转移式来做决策。

fv = Math.max(fv, fv-c\[i] + wi);

根据这个式子可知,第二个物品可能被放入,也可能不被放入。

fv代表第二个物品不被放入。fv-c\[i]代表第二个物品被放入。

那么第二个物品被放入,背包中的物品会不会被丢弃呢?

首先,由于背包容量越大,背包内可装的东西价值越多。

所以fv>=fv-c\[i]

  • 当fv = fv-c\[i],则不需要丢弃东西持续往背包中装东西。
  • 当fv > fv-c\[i],则表示我们用i物品替换了等体积的物品。我们不知道是哪个物品。但是 max(f[v],f[v-c[i]]+w[i])能保证我们每次的替换都会使背包内的东西价值更高。
  • 那么当我们循环了i次,就用i个物品把背包内的价值替换了一遍,背包内的东西必然就是当前最高价值的。

总结

01背包,是二维的。二维是因为每个物品都只能选一次。可以简化成一维,但是需要反向遍历避免重复增加的情况。完全背包是因为存在重复增加的情况,所以直接用一维正序。

本文参考

leetcode.cn/problems/co...

在leecode刷题时看了这个大佬的文章,理解了背包的基本概念,再加入了自己的想法进行推演。

相关推荐
倒头就睡的小比特4 天前
算法竞赛C++常用的STL
c++·算法
小羊没烦恼!4 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
猎头南楼4 天前
知识社区推荐系统实践:新用户冷启动与长短期兴趣建模的挑战 资深推荐算法工程师
人工智能·深度学习·算法·机器学习
旖旎夜光4 天前
力控面试题 01.01: 判定字符是否唯一(位运算) —— 题解
c++·学习·算法·leetcode·力控
wzdark4 天前
大规模并行计算中的负载均衡算法研究4
算法
Because_of_Her14 天前
并查集-听课笔记
笔记·算法·并查集
码流子4 天前
高速公路安全监测实践:碰撞监测预警+物联网底座,从感知到处置的闭环
大数据·人工智能·物联网·算法·架构
another heaven4 天前
【算法/C++ MD5算法能否逆解码?原理、C++实现与同类哈希算法对比】
c++·算法·哈希算法
wzdark4 天前
从算法设计模式看编程思维的抽象能力4
算法
2601_962218614 天前
万象生鲜系统称重自动多退少补算法解决生鲜非标品痛点
大数据·数据库·人工智能·python·算法