1. 概述

背包问题是一个历史悠久且广泛应用的优化问题。在本教程中,我们会介绍背包问题的几个变种,并重点讨论 0-1 背包问题

我们还会解释为什么 0-1 背包问题是 NP-Complete 的,以及如何使用 动态规划算法伪多项式时间 内求解它。


2. 问题定义

给定一个最大承重为 W 的背包,以及一组物品,每个物品都有自己的重量 w[i] 和价值 v[i]。我们的目标是:

在不超过背包承重的前提下,选取物品使得总价值最大。

这是一个典型的优化问题,数学表达如下:

  • 目标函数:max Σ(v[i] * x[i]),其中 x[i] ∈ {0, 1}
  • 约束条件:Σ(w[i] * x[i]) ≤ W

3. 背包问题的变种

根据物品是否可以重复选取,背包问题有以下三种变种:

3.1 0-1 背包问题(0-1 Knapsack)

每个物品只能选一次或不选:

  • x[i] ∈ {0, 1}

这是最基础也是最经典的形式,本教程将重点讲解。

3.2 有界背包问题(Bounded Knapsack)

每个物品最多可以选 c 次:

  • 0 ≤ x[i] ≤ c

适用于有限库存的场景。

3.3 无界背包问题(Unbounded Knapsack)

每个物品可以无限次选取:

  • x[i] ≥ 0

适用于物品无限供应的情况。


4. 为什么 0-1 背包问题是 NP-Complete?

判断版本的 0-1 背包问题属于 NP-Complete 类:

给定物品的重量和价值,是否存在一个子集,使得总重量不超过 W 且总价值至少为 V

这个问题可以归约到经典的 子集和问题(Subset Sum Problem),而子集和问题是已知的 NP-Complete 问题。

因此,0-1 背包问题也是 NP-Complete 的。
不过,它可以在 伪多项式时间 内求解,因为其复杂度与 W 的数值大小相关,而不是其二进制位数。


5. 动态规划解法(DP)

我们使用一个二维数组 dp[i][w] 表示前 i 个物品、总重量不超过 w 时的最大价值。

状态转移方程:

if w[i] > w:
    dp[i][w] = dp[i-1][w]
else:
    dp[i][w] = max(dp[i-1][w], dp[i-1][w - w[i]] + v[i])

初始化:

  • dp[0][w] = 0(没有物品,价值为 0)
  • dp[i][0] = 0(容量为 0,无法装物品)

时间复杂度:

O(n * W)
⚠️ 注意:W 是重量上限,不是输入长度的函数,因此该算法属于 伪多项式时间


6. 示例演示

我们有如下物品:

物品编号 重量 价值
1 1 10
2 2 40
3 1 20
4 3 20
5 5 60

背包最大承重为 W = 10

我们使用动态规划构造如下 DP 表格:

weight →     0   1   2   3   4   5   6   7   8   9   10
---------------------------------------------------------
物品0        0   0   0   0   0   0   0   0   0   0   0
物品1(1,10)  0  10  10  10  10  10  10  10  10  10  10
物品2(2,40)  0  10  40  50  50  50  50  50  50  50  50
物品3(1,20)  0  20  40  60  70  70  70  70  70  70  70
物品4(3,20)  0  20  40  60  70  70  80  90  90  90  90
物品5(5,60)  0  20  40  60  70  70  80 100 120 130 130

最终结果:

最大价值为 130,当背包承重为 10 时


7. 时间复杂度分析

  • 初始化:O(n + W)
  • 两层循环:O(n * W)
  • 每次状态转移:O(1)

总时间复杂度为 O(nW)

⚠️ 注意:虽然看起来是多项式时间,但 W 是数值大小,不是输入长度的函数。因此它是 伪多项式时间

例如,当 W100 增加到 1000,虽然数值只增加了 10 倍,但二进制表示从 7 位增加到 10 位,处理时间呈指数增长。


8. 总结

  • 0-1 背包问题是经典的 NP-Complete 问题。
  • 可以使用 动态规划O(nW) 时间内求解。
  • 由于复杂度依赖于数值大小,因此属于 伪多项式时间算法
  • 适用于物品数量不大、重量上限适中的场景。

💡 踩坑提示

  • 不要使用递归暴力解法,会超时。
  • 优化空间复杂度时可使用滚动数组。
  • 注意初始化边界条件,否则容易出错。

原始标题:0-1 Knapsack: A Problem With NP-Completeness and Solvable in Pseudo-Polynomial Time