背包问题 (Knapsack Problem)
背包问题是动态规划中最经典的模型。
0/1 背包问题
问题描述: 有一个背包,容量为 \(W\)。有 \(N\) 种物品,每种物品只有一件。第 \(i\) 件物品的重量是 \(wt[i]\),价值是 \(val[i]\)。求解将哪些物品装入背包可使价值总和最大。
特点:每种物品要么选 (1),要么不选 (0)。
状态定义
dp[i][w] 表示:对于前 \(i\) 个物品,在背包容量为 \(w\) 时能拿到的最大价值。
转移方程
对于第 \(i\) 个物品:
- 不装:
dp[i][w] = dp[i-1][w](继承前 \(i-1\) 个的结果) - 装:
dp[i][w] = dp[i-1][w - wt[i]] + val[i](前提是能装下,即 \(w \ge wt[i]\))
取两者的最大值。
二维数组版本
def knapsack_01(W, wt, val, n):
# dp[i][j] 大小为 (n+1) x (W+1)
dp = [[0] * (W + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for w in range(1, W + 1):
if w - wt[i-1] < 0:
# 装不下,只能不选
dp[i][w] = dp[i-1][w]
else:
# 选 或 不选,取最大值
dp[i][w] = max(
dp[i-1][w],
dp[i-1][w - wt[i-1]] + val[i-1]
)
return dp[n][W]
一维数组优化 (空间压缩)
因为 dp[i] 只依赖于 dp[i-1],我们可以把状态压缩成一行。
注意:为了防止使用本层计算过的新结果覆盖上一层的数据,内层循环需要倒序遍历。
def knapsack_01_optimized(W, wt, val, n):
dp = [0] * (W + 1)
for i in range(n):
# 必须倒序遍历!
for w in range(W, wt[i] - 1, -1):
dp[w] = max(dp[w], dp[w - wt[i]] + val[i])
return dp[W]
# 测试
W = 4
wt = [2, 1, 3]
val = [4, 2, 3]
print(knapsack_01_optimized(W, wt, val, 3)) # 6 (选物品 0 和 1)
完全背包问题
问题描述: 与 0/1 背包类似,但每种物品有无限件可用。
解法: 唯一的区别在于一维数组优化时的遍历顺序。
- 0/1 背包:倒序遍历 (保证每个物品只拿一次)。
- 完全背包:正序遍历 (允许一个物品被多次放入)。
def knapsack_complete(W, wt, val, n):
dp = [0] * (W + 1)
for i in range(n):
# 正序遍历
for w in range(wt[i], W + 1):
dp[w] = max(dp[w], dp[w - wt[i]] + val[i])
return dp[W]
本章小结
| 问题类型 | 特点 | 遍历顺序 (一维数组) |
|---|---|---|
| 0/1 背包 | 每个物品限 1 个 | 外层遍历物品,内层倒序遍历容量 |
| 完全背包 | 每个物品无限个 | 外层遍历物品,内层正序遍历容量 |
这是各类背包变体(如多重背包、分组背包)的基础。
下一章:贪心算法