1. 分组背包问题概述
分组背包问题是动态规划中一个经典变种,它扩展了传统0-1背包问题的约束条件。在实际开发中,我经常遇到需要从多个互斥的选项组中选择最优组合的场景,比如广告投放策略选择、课程选修方案设计等。这类问题的核心特征是:物品被划分为若干组,每组内的物品相互冲突,最多只能选择其中一个。
与基础背包问题相比,分组背包的决策维度从单个物品升级到了物品组。这种约束在实际业务中非常常见——比如电商促销时,同一品类的优惠券往往只能选一张使用。理解这个模型的关键在于抓住"组内互斥"这个核心特征。
2. 问题建模与状态定义
2.1 标准问题描述
给定N组物品和容量为V的背包:
- 每组物品有若干个,同一组内物品最多选一个
- 每个物品有体积v[i][j]和价值w[i][j]
- 求不超过背包容量的最大价值
这个模型可以形式化为:
max Σw[i][j]*x[i][j] s.t. Σv[i][j]*x[i][j] ≤ V Σx[i][j] ≤ 1 for each group i x[i][j] ∈ {0,1}2.2 状态转移方程推导
定义dp[i][j]为考虑前i组物品,背包容量为j时的最大价值。状态转移需要分情况讨论:
- 不选第i组任何物品:dp[i][j] = dp[i-1][j]
- 选第i组的某个物品k:dp[i][j] = max(dp[i-1][j-v[i][k]] + w[i][k])
综合起来得到:
dp[i][j] = max{ dp[i-1][j], max(dp[i-1][j-v[i][k]] + w[i][k]) for all k in group i }这个方程体现了分组背包的核心思想——对每组物品做"选或不选"的决策,如果选则必须遍历组内所有物品的可能性。
3. 算法实现细节
3.1 基础实现版本
def group_knapsack(V, groups): n = len(groups) dp = [[0]*(V+1) for _ in range(n+1)] for i in range(1, n+1): for j in range(V+1): dp[i][j] = dp[i-1][j] # 不选该组 for v, w in groups[i-1]: if j >= v: dp[i][j] = max(dp[i][j], dp[i-1][j-v] + w) return dp[n][V]这个实现的时间复杂度是O(NVS),其中S是平均每组物品数。空间复杂度O(NV)。
3.2 空间优化技巧
观察到dp[i]只依赖dp[i-1],可以优化为一维数组:
def group_knapsack_opt(V, groups): dp = [0]*(V+1) for group in groups: for j in range(V, -1, -1): # 逆向遍历 for v, w in group: if j >= v: dp[j] = max(dp[j], dp[j-v] + w) return dp[V]注意:内层循环必须逆向遍历,否则会重复计算同一组的多个物品
4. 典型应用场景
4.1 课程选修问题
假设某计算机专业有以下课程分组:
- 编程语言组:Java(4学分,8价值)、Python(3学分,7价值)
- 算法组:DP(3学分,9价值)、图论(4学分,10价值)
- 系统组:OS(5学分,12价值)、网络(4学分,11价值)
学生最多选修20学分,每组最多选一门。这就是典型的分组背包问题。
4.2 广告投放优化
某广告平台有:
- 视频广告组:15s(成本5,收益8)、30s(成本8,收益12)
- 图文广告组:单图(成本3,收益5)、多图(成本6,收益9)
- 文字广告组:短文案(成本2,收益3)、长文案(成本4,收益6)
预算限制为20,每组最多投一个广告。用分组背包可以求出最优组合。
5. 常见问题与调试技巧
5.1 初始化陷阱
错误做法:
dp = [[0]*V]*n # 浅拷贝问题!正确初始化:
dp = [[0 for _ in range(V+1)] for _ in range(n+1)]5.2 边界条件处理
常见错误包括:
- 忘记处理j<v[i][k]的情况
- 组索引从0还是1开始混淆
- 容量遍历顺序错误(必须从大到小)
5.3 性能优化建议
当V很大时:
- 先过滤掉组内明显劣势的物品(v大w小的)
- 对每组物品按v排序,利用单调性剪枝
- 考虑使用滚动数组或位运算优化
6. 扩展变种与实践
6.1 每组必选一件
修改状态转移方程:
dp[i][j] = max(dp[i-1][j-v[i][k]] + w[i][k]) # 去掉"不选该组"的情况6.2 多维费用约束
增加状态维度:
dp = [[[0]*(V2+1) for _ in range(V1+1)] for _ in range(n+1)]6.3 混合背包问题
结合完全背包、多重背包等情况,需要分情况处理不同组别的物品类型。
在实际项目中,我遇到过需要处理200+组、总容量10^6级别的分组背包问题。关键突破点是:
- 预处理阶段合并相似物品
- 使用快速最大值查询数据结构
- 并行化处理独立组别
分组背包的灵活建模能力,使其成为解决实际约束优化问题的利器。掌握其核心思想后,可以扩展到更复杂的业务场景中。