动态规划经典问题:背包问题的五种变种解析
背包问题是动态规划中的经典问题,在实际应用中存在多种变种。本文将深入解析五种常见的背包问题变种,包括恰好装满背包、方案总数、最优方案数、具体方案以及字典序最小的具体方案。
1. 恰好装满背包的最大价值
问题定义
在给定背包容量和物品重量、价值的情况下,要求恰好装满背包时能获得的最大价值总和。
关键思路
与普通背包问题不同,恰好装满背包需要在初始化时进行特殊处理:
- 只有容量为0的背包可以被"恰好装满"(不装任何物品),因此初始化
dp[0] = 0
- 其他容量的背包初始状态设为
-∞
,表示无法恰好装满
代码实现
def zeroOnePackJustFillUp(weight, value, W):
size = len(weight)
dp = [float('-inf')] * (W + 1)
dp[0] = 0
for i in range(1, size + 1):
for w in range(W, weight[i - 1] - 1, -1):
dp[w] = max(dp[w], dp[w - weight[i - 1]] + value[i - 1])
return dp[W] if dp[W] != float('-inf') else -1
复杂度分析
- 时间复杂度:O(n×W)
- 空间复杂度:O(W)
2. 背包问题的方案总数
问题定义
计算在不超过背包容量的情况下,有多少种不同的物品组合方式。
关键思路
将状态转移方程中的"求最大值"改为"求和":
- 初始化
dp[0] = 1
(容量为0时有1种方案:不装任何物品) - 状态转移方程:
dp[w] += dp[w - weight[i-1]]
代码实现
def zeroOnePackNumbers(weight, value, W):
size = len(weight)
dp = [0] * (W + 1)
dp[0] = 1
for i in range(1, size + 1):
for w in range(W, weight[i - 1] - 1, -1):
dp[w] += dp[w - weight[i - 1]]
return dp[W]
复杂度分析
- 时间复杂度:O(n×W)
- 空间复杂度:O(W)
3. 最优方案的数量
问题定义
在不超过背包容量的前提下,计算能够获得最大价值的方案数量。
关键思路
需要同时维护两个状态数组:
dp[w]
记录最大价值count[w]
记录对应价值的方案数
状态转移时需要考虑三种情况:
- 选择当前物品获得更高价值:更新价值和方案数
- 两种选择获得相同价值:累加方案数
- 不选择当前物品获得更高价值:保持原有方案数
代码实现
def zeroOnePackMaxProfitNumbers(weight, value, W):
size = len(weight)
dp = [0] * (W + 1)
count = [1] * (W + 1)
for i in range(1, size + 1):
for w in range(W, weight[i - 1] - 1, -1):
if dp[w] < dp[w - weight[i - 1]] + value[i - 1]:
dp[w] = dp[w - weight[i - 1]] + value[i - 1]
count[w] = count[w - weight[i - 1]]
elif dp[w] == dp[w - weight[i - 1]] + value[i - 1]:
count[w] += count[w - weight[i - 1]]
return count[W]
复杂度分析
- 时间复杂度:O(n×W)
- 空间复杂度:O(W)
4. 具体方案输出
问题定义
不仅要求最大价值,还需要输出具体选择了哪些物品。
关键思路
使用额外的path
数组记录状态转移路径:
path[i][w]
标记是否选择了第i件物品- 从最终状态逆向追踪,重构具体方案
代码实现
def zeroOnePackPrintPath(weight, value, W):
size = len(weight)
dp = [[0] * (W + 1) for _ in range(size + 1)]
path = [[False] * (W + 1) for _ in range(size + 1)]
for i in range(1, size + 1):
for w in range(W + 1):
if w < weight[i - 1]:
dp[i][w] = dp[i - 1][w]
path[i][w] = False
else:
if dp[i - 1][w] < dp[i - 1][w - weight[i - 1]] + value[i - 1]:
dp[i][w] = dp[i - 1][w - weight[i - 1]] + value[i - 1]
path[i][w] = True
elif dp[i - 1][w] == dp[i - 1][w - weight[i - 1]] + value[i - 1]:
dp[i][w] = dp[i - 1][w]
path[i][w] = True
else:
dp[i][w] = dp[i - 1][w]
path[i][w] = False
res = []
i, w = size, W
while i >= 1 and w >= 0:
if path[i][w]:
res.append(str(i - 1))
w -= weight[i - 1]
i -= 1
return " ".join(res[::-1])
复杂度分析
- 时间复杂度:O(n×W)
- 空间复杂度:O(n×W)
5. 字典序最小的具体方案
问题定义
在输出具体方案时,要求物品编号的字典序最小。
关键思路
- 先将物品顺序反转,使得优先考虑原序号小的物品
- 使用与普通具体方案相同的追踪方法
- 输出时再将序号转换回来
代码实现
def zeroOnePackPrintPathMinOrder(weight, value, W):
size = len(weight)
weight = weight[::-1]
value = value[::-1]
dp = [[0] * (W + 1) for _ in range(size + 1)]
path = [[False] * (W + 1) for _ in range(size + 1)]
for i in range(1, size + 1):
for w in range(W + 1):
if w < weight[i - 1]:
dp[i][w] = dp[i - 1][w]
path[i][w] = False
else:
if dp[i - 1][w] < dp[i - 1][w - weight[i - 1]] + value[i - 1]:
dp[i][w] = dp[i - 1][w - weight[i - 1]] + value[i - 1]
path[i][w] = True
elif dp[i - 1][w] == dp[i - 1][w - weight[i - 1]] + value[i - 1]:
dp[i][w] = dp[i - 1][w]
path[i][w] = True
else:
dp[i][w] = dp[i - 1][w]
path[i][w] = False
res = []
i, w = size, W
while i >= 1 and w >= 0:
if path[i][w]:
res.append(str(size - i))
w -= weight[i - 1]
i -= 1
return " ".join(res)
复杂度分析
- 时间复杂度:O(n×W)
- 空间复杂度:O(n×W)
总结
本文详细介绍了背包问题的五种常见变种及其解决方案。理解这些变种的关键在于:
- 明确问题要求(最大价值、方案数、具体方案等)
- 设计合适的状态表示和转移方程
- 根据需求调整初始条件和状态转移逻辑
掌握这些变种可以帮助我们更好地应对实际应用中各种复杂的背包问题场景。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考