第52课 - 背包问题
学习目标
完成本课学习后,你将能够:
- 理解背包问题的基本概念和分类
- 掌握0-1背包问题的状态转移方程和解法
- 区分0-1背包和完全背包的区别与实现
- 运用背包思想解决子集和等问题
- 分析背包问题的时间和空间复杂度
核心概念
什么是背包问题?
想象你是一个小偷,面前有一堆物品,每个物品有重量和价值。你有一个承重有限的背包,需要选择一些物品装进背包,使得总价值最大。这就是经典的背包问题。
背包问题主要分为两类:
- 0-1背包:每个物品只能选或不选(不能重复选)
- 完全背包:每个物品可以选无限次
状态转移思想
解决背包问题的核心思想是动态规划。我们需要定义一个二维数组 dp[i][w],表示:
i:考虑前i个物品w:当前背包剩余容量dp[i][w]:此时能获得的最大价值
对于每个物品,我们有两种选择:
- 不选第i个物品:
dp[i][w] = dp[i-1][w] - 选第i个物品(前提是能放下):
dp[i][w] = dp[i-1][w-weight[i]] + value[i]
最终结果取两者中的最大值。
代码示例
1. 0-1背包问题
def knapsack_01(weights, values, capacity):
"""
0-1背包问题
:param weights: 物品重量列表
:param values: 物品价值列表
:param capacity: 背包容量
:return: 最大价值
"""
n = len(weights)
# 创建DP表,dp[i][w]表示前i个物品、容量为w时的最大价值
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
for i in range(1, n + 1): # 遍历物品
for w in range(capacity + 1): # 遍历容量
# 情况1:不选第i个物品
dp[i][w] = dp[i-1][w]
# 情况2:选第i个物品(如果容量足够)
if w >= weights[i-1]:
# 注意:values和weights是0-indexed,所以用i-1
choose = dp[i-1][w - weights[i-1]] + values[i-1]
dp[i][w] = max(dp[i][w], choose)
return dp[n][capacity]
# 测试
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
capacity = 8
print("0-1背包最大价值:", knapsack_01(weights, values, capacity)) # 输出: 10
2. 完全背包问题
def knapsack_unbounded(weights, values, capacity):
"""
完全背包问题(每个物品可选无限次)
:param weights: 物品重量列表
:param values: 物品价值列表
:param capacity: 背包容量
:return: 最大价值
"""
n = len(weights)
# 优化:只需要一维数组
dp = [0] * (capacity + 1)
for i in range(n): # 遍历物品
for w in range(weights[i], capacity + 1): # 从小到大遍历容量
# 状态转移:选第i个物品
dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
return dp[capacity]
# 测试
print("完全背包最大价值:", knapsack_unbounded(weights, values, capacity)) # 输出: 12
3. 子集和问题(背包问题的变种)
def subset_sum(nums, target):
"""
判断是否存在子集,使其和等于目标值
:param nums: 数字列表
:param target: 目标和
:return: 是否存在这样的子集
"""
# dp[i][j]表示:使用前i个数字,能否组成和为j
n = len(nums)
dp = [[False] * (target + 1) for _ in range(n + 1)]
# 初始化:不选任何数字时,和为0是可行的
for i in range(n + 1):
dp[i][0] = True
for i in range(1, n + 1):
for j in range(1, target + 1):
# 不选第i个数字
dp[i][j] = dp[i-1][j]
# 选第i个数字(如果j >= nums[i-1])
if j >= nums[i-1] and dp[i-1][j - nums[i-1]]:
dp[i][j] = True
return dp[n][target]
# 测试
nums = [3, 34, 4, 12, 5, 2]
target = 9
print(f"是否存在和为{target}的子集:", subset_sum(nums, target)) # 输出: True (4+5)
实践练习
练习1:基础0-1背包
问题:有3个物品,重量分别为[1, 2, 3],价值分别为[6, 10, 12],背包容量为5。求最大价值。
预期输出:22
提示:可以直接使用上面的 knapsack_01 函数。
练习2:完全背包问题
问题:有3种物品,重量分别为[2, 3, 5],价值分别为[3, 4, 6],背包容量为10。每种物品可以选无限次,求最大价值。
预期输出:15
要求:实现一个一维DP数组的解法。
练习3:子集划分问题
问题:给定一个只包含正整数的非空数组,判断是否可以将其分割为两个元素和相等的子集。 示例:[1, 5, 11, 5] → True(可以分成[1, 5, 5]和[11])
预期输出:True
提示:这实际上是背包问题,目标是找出和为总和一半的子集。
常见错误
-
混淆0-1背包和完全背包的遍历顺序
- 0-1背包:内层循环从大到小遍历容量(防止重复选择)
- 完全背包:内层循环从小到大遍历容量(允许重复选择)
-
DP数组初始化错误
- 对于最大价值问题,应该初始化为0
- 对于可行性问题(如子集和),需要正确初始化
dp[0][0] = True
-
状态转移方程记忆错误
- 正确:
dp[i][w] = max(dp[i-1][w], dp[i-1][w-weight] + value) - 错误:使用
dp[i][w-weight](这会导致无限背包的效果)
- 正确:
-
忽略边界条件
- 当
w < weight[i]时,只能选择不拿第i个物品 - 当
i = 0时(没有物品可选),所有dp[0][w]都应该为0
- 当
-
空间优化时的错误
- 一维优化时,遍历方向错误会导致结果错误
- 忘记从
weight开始遍历,导致数组越界
小结
本课我们学习了背包问题这一经典的动态规划问题:
- 背包问题本质:在有限容量下做出最优选择
- 状态定义:
dp[i][w]表示前i个物品、容量为w时的最优解 - 转移方程:
dp[i][w] = max(不选, 选) - 两类变种:
- 0-1背包:物品只能选一次
- 完全背包:物品可选无限次
- 空间优化:完全背包可以优化为一维数组
- 扩展应用:子集和问题、分割等和子集等问题
背包问题是动态规划的重要基础,掌握了它的思想,可以帮助我们解决很多类似的优化问题。下一课我们将学习最长公共子序列,这是另一类经典的动态规划问题。
练习编辑器
rust
Loading...