谈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刷题时看了这个大佬的文章,理解了背包的基本概念,再加入了自己的想法进行推演。

相关推荐
折哥的程序人生 · 物流技术专研10 小时前
Java面试85题图解版 · 特别篇:2026后端高频面试题复盘(算法底层逻辑+高并发架构设计全解析,附Java实战代码)
java·网络·数据库·算法·面试
想吃火锅100511 小时前
【leetcode】14.最长公共前缀js
算法·leetcode·职场和发展
云絮.12 小时前
数据库操作
数据库·mysql·算法·oracle
小林ixn12 小时前
LeetCode 206. 反转链表(迭代 + 递归详解)
算法·leetcode·链表
凡人叶枫12 小时前
Effective C++ 条款17:以独立语句将 newed 对象置入智能指针
java·linux·开发语言·c++·算法
菜鸟‍14 小时前
LeetCode 1 27 和 704 || 两数之和 移除元素 二分查找
算法·leetcode·职场和发展
退休倒计时15 小时前
【每日一题】LeetCode 142. 环形链表 II TypeScript
算法·leetcode·链表·typescript
popcorn_min15 小时前
Digits 手写数字识别:随机森林多分类 + 像素级特征热力图
算法·随机森林·分类
liulilittle16 小时前
拥塞控制:排水终止的两种决策:OR 与 AND
网络·tcp/ip·计算机网络·算法·信息与通信·tcp·通信
weixin_3077791317 小时前
从脚本执行到智能体协作:AI辅助测试能力的范式重构
运维·开发语言·人工智能·算法·测试用例