0/1背包问题详解:从暴力递归到动态规划
一、问题描述
经典场景
假设有一个背包,它的容量为 W(如5kg)。有 n 个物品,每个物品有两个属性:
- 重量(weight):如2kg、3kg等
- 价值(value):如6元、10元等
限制条件:每个物品只能选择放入背包一次(0/1选择),且总重量不能超过背包容量。
目标:如何选择物品,使得背包中物品的总价值最大?
数学表达
给定一组物品,每个物品有重量 wi 和价值 vi,背包容量为 W,求一个子集 S⊆1,2,…,n,使得:
且max∑i∈Svi且∑i∈Swi≤W
二、暴力递归解法
思路分析
对于每个物品,有两种选择:
- 放入背包:前提是当前背包剩余容量足够
- 不放入背包
通过递归遍历所有可能的选择组合,找到价值最大的方案。
代码实现
def knapsack_recursive(weights, values, W, n):
# 基本情况:没有物品或背包容量为0
if n == 0 or W == 0:
return 0
# 如果当前物品重量超过背包容量,则不能放入
if weights[n-1] > W:
return knapsack_recursive(weights, values, W, n-1)
# 否则,考虑两种情况:放入或不放入当前物品
else:
# 选择放入当前物品
included = values[n-1] + knapsack_recursive(weights, values, W - weights[n-1], n-1)
# 选择不放入当前物品
excluded = knapsack_recursive(weights, values, W, n-1)
# 返回两种情况中的较大值
return max(included, excluded)
# 示例使用
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
W = 8 # 背包容量
n = len(weights)
max_value = knapsack_recursive(weights, values, W, n)
print(f"最大价值: {max_value}")
复杂度分析
- 时间复杂度:O(2n ),因为每个物品有两种选择,递归树的节点数为 2n
- 空间复杂度:O(n ),递归栈的深度最大为 n
三、动态规划解法
思路分析
暴力递归存在大量重复计算,可以使用动态规划(DP)通过表格记录中间结果,避免重复计算。
定义二维数组 dp[i][w] 表示:
- 前 i 个物品
- 背包容量为 w
- 能获得的最大价值
代码实现
def knapsack_dp(weights, values, W):
n = len(weights)
# 创建一个二维数组 dp,dp[i][w] 表示前i个物品在容量w下的最大价值
dp = [[0 for _ in range(W + 1)] for _ in range(n + 1)]
# 填充dp数组
for i in range(1, n + 1):
for w in range(1, W + 1):
# 如果当前物品重量大于当前容量,则不能放入
if weights[i-1] > w:
dp[i][w] = dp[i-1][w]
else:
# 否则,考虑放入或不放入当前物品,取最大值
dp[i][w] = max(dp[i-1][w], # 不放入
dp[i-1][w - weights[i-1]] + values[i-1]) # 放入
# 返回最终结果
return dp[n][W]
# 示例使用
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
W = 8
max_value = knapsack_dp(weights, values, W)
print(f"最大价值: {max_value}")
复杂度分析
- 时间复杂度:O(nW ),其中 n 是物品数量,W 是背包容量
- 空间复杂度:O(nW ),主要用于存储二维数组
四、空间优化的动态规划解法
思路分析
观察状态转移方程,发现 dp[i][w] 只依赖于 dp[i−1][w] 和 dp[i−1][w−wi],可以将二维数组优化为一维数组。
关键技巧:
- 使用一维数组 dp[w] 表示当前容量为 w 时的最大价值
- 遍历容量时从大到小倒序,确保每个物品只被考虑一次
代码实现
def knapsack_dp_optimized(weights, values, W):
n = len(weights)
# 创建一个一维数组 dp,dp[w] 表示容量为w时的最大价值
dp = [0 for _ in range(W + 1)]
# 遍历每个物品
for i in range(n):
# 倒序遍历容量,从W到weights[i]
for w in range(W, weights[i] - 1, -1):
# 考虑放入或不放入当前物品,取最大值
dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
# 返回最终结果
return dp[W]
# 示例使用
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
W = 8
max_value = knapsack_dp_optimized(weights, values, W)
print(f"最大价值: {max_value}")
复杂度分析
- 时间复杂度:O(nW ),与二维数组解法相同
- 空间复杂度:O(W ),仅需一维数组
五、总结
三种解法对比
解法 | 时间复杂度 | 空间复杂度 | 适用场景 |
---|---|---|---|
暴力递归 | O(2n ) | O(n ) | 小规模问题 |
二维DP数组 | O(nW ) | O(nW ) | 中等规模问题 |
一维DP数组 | O(nW ) | O(W ) | 大规模问题(推荐) |
0/1背包问题的变形
- 完全背包:每个物品可以无限次选取
- 多重背包:每个物品有固定的数量限制
- 二维费用背包:除了重量限制,还有体积等其他限制
掌握0/1背包问题的基本解法是理解更复杂背包问题的基础,建议通过练习题加深理解!
练习题推荐
希望本文能帮助你彻底理解0/1背包问题!