混合背包问题实战:如何用Python动态规划解决三种背包组合问题?
如果你刷过LeetCode或者准备过算法面试,大概率遇到过背包问题。从经典的“0-1背包”到“完全背包”,再到“多重背包”,每个变种都像是一道独立的关卡,考验着你对动态规划核心思想的理解。但真正的挑战往往出现在这些基础问题的混合体上——当一道题目里同时出现“只能选一次”、“可以选无限次”和“最多选有限次”这三种物品时,很多人的思路就容易陷入混乱。
这就是混合背包问题。它不像单一类型的背包问题那样有固定的模板可以套用,而是要求你根据每个物品的特性,灵活切换不同的状态转移策略。听起来复杂,但只要你真正理解了三种基础背包问题的本质区别,混合背包其实只是将它们有机地组合在一起。今天,我们就从实战编码的角度,用Python彻底拆解这个问题,不仅提供清晰的代码模板,还会深入探讨二进制优化、滚动数组压缩等关键技巧,并对比不同实现方式的性能差异,让你在竞赛或面试中遇到这类问题时能够从容应对。
1. 理解混合背包:三种基础背包问题的融合
在深入代码之前,我们必须先厘清混合背包问题的本质。所谓“混合”,指的是在一个问题中,物品的类型不是单一的,而是包含了三种经典背包问题的约束条件。
1.1 三种基础背包问题的核心差异
让我们先快速回顾一下三种基础背包问题的定义和核心状态转移方程:
-
0-1背包:每种物品只有一件(要么选,要么不选)
- 状态转移:
dp[j] = max(dp[j], dp[j - weight] + value)(逆序遍历容量) - 关键:每个物品只能使用一次,避免重复选择
- 状态转移:
-
完全背包:每种物品有无限件
- 状态转移:
dp[j] = max(dp[j], dp[j - weight] + value)(正序遍历容量) - 关键:允许重复选择同一物品
- 状态转移:
-
多重背包:每种物品有有限件(比如最多选s件)
- 状态转移:可转化为多个0-1背包问题,或使用二进制优化
- 关键:在0-1和完全背包之间的一种中间状态
这三种问题的状态转移方程看起来相似,但遍历顺序和物品处理方式有本质区别。理解这一点是解决混合背包问题的前提。
1.2 混合背包的问题定义
混合背包问题的标准描述通常如下:
有 N 种物品和一个容量为 V 的背包。第 i 种物品的体积是 v[i],价值是 w[i],数量限制为 s[i]。
- 如果 s[i] = -1,表示该物品只能使用1次(0-1背包)
- 如果 s[i] = 0,表示该物品可以使用无限次(完全背包)
- 如果 s[i] > 0,表示该物品最多可以使用 s[i] 次(多重背包)
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输入样例:
4 5 # 4种物品,背包容量5
1 2 -1 # 物品1:体积1,价值2,只能选1次
2 4 1 # 物品2:体积2,价值4,最多选1次(等价0-1)
3 4 0 # 物品3:体积3,价值4,可以选无限次
4 5 2 # 物品4:体积4,价值5,最多选2次
输出样例:
8
这个例子中,我们同时面对三种不同类型的物品,需要针对每种类型采用不同的处理策略。
1.3 混合背包的直观理解
为了更直观地理解混合背包,我们可以把它想象成一个智能的购物决策过程:
| 物品类型 | 现实类比 | 处理策略 |
|---|---|---|
| 0-1背包 | 限量版商品,每人限购1件 | 要么买,要么不买 |
| 完全背包 | 普通商品,库存充足 | 想买多少就买多少(只要钱够) |
| 多重背包 | 促销商品,每人限购s件 | 最多买s件,可以买0~s件 |
在混合背包问题中,你的购物车里可能同时有这三种商品,需要在预算(背包容量)有限的情况下,做出最优的购买组合。
2. 基础解法:分类处理与状态转移
最直接的思路就是“分类讨论”——根据每个物品的类型,调用对应的处理函数。这种方法虽然朴素,但清晰地体现了混合背包问题的本质。
2.1 算法框架设计
混合背包问题的核心算法框架可以概括为以下伪代码:
def mixed_knapsack(N, V, items):
# items: 列表,每个元素为(v, w, s)
dp = [0] * (V + 1) # 一维DP数组
for i in range(N):
v, w, s = items[i]
if s == -1: # 0-1背包
zero_one_pack(dp, v, w, V)
elif s == 0: # 完全背包
complete_pack(dp, v, w, V)
else: # 多重背包
multiple_pack(dp, v, w, s, V)
return dp[V]
这个框架的关键在于三个核心函数的实现,以及它们如何共享同一个DP数组。
2.2 三种背包处理函数的实现
让我们分别实现这三个核心函数:
def zero_one_pack(dp, v, w, V):
"""处理0-1背包问题"""
for j in range(V, v - 1, -1): # 逆序遍历
dp[j] = max(dp[j], dp[j - v] + w)
def complete_pack(dp, v, w, V):
"""处理完全背包问题"""
for j in range(v, V + 1): # 正序遍历
dp[j] = max(dp[j], dp[j - v] + w)
def multiple_pack(dp, v, w, s, V):
"""处理多重背包问题(二进制优化版)"""
if v * s >= V: # 如果物品总体积超过背包容量,相当于完全背包
complete_pack(dp, v, w, V)
return
# 二进制拆分
k = 1
while k <= s:
zero_one_pack(dp, k * v, k * w, V)
s -= k
k <<= 1 # k *= 2
# 处理剩余部分
if s > 0:
zero_one_pack(dp, s * v, s * w, V)
这里需要特别注意的是遍历顺序的区别:
- 0-1背包:逆序遍历(从V到v),确保每个物品只被选择一次
- 完全背包:正序遍历(从v到V),允许同一物品被多次选择
- 多重背包:通过二进制优化转化为多个0-1背包问题
2.3 完整的基础实现
将上述组件组合起来,我们得到完整的混合背包解法:
def mixed_knapsack_basic(N, V, it


935

被折叠的 条评论
为什么被折叠?



