分组背包
问题描述
有 nnn 组物品,每组物品有若干个,每组物品中最多只能选择一个。每个物品有重量 wijw_{ij}wij 和价值 vijv_{ij}vij,其中 iii 表示组号,jjj 表示组内编号。现在有一个容量为 WWW 的背包,求解在不超过背包容量的情况下,能够装入物品的最大价值。
与其他背包的区别
- 01背包:每个物品最多选择1次
- 完全背包:每个物品可以选择无限次
- 多重背包:每个物品可以选择有限次
- 分组背包:物品分组,每组最多选择1个
动态规划解法
状态定义
- dp[i][j]dp[i][j]dp[i][j] 表示考虑前 iii 组物品,背包容量为 jjj 时能获得的最大价值
状态转移方程
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i][k]] + v[i][k])
其中 k 为第 i 组中的物品编号
代码实现
C++ 实现
int groupKnapsack(vector<vector<int>>& w, vector<vector<int>>& v, int W) {
int n = w.size(); // 组数
vector<int> dp(W + 1, 0);
for (int i = 0; i < n; i++) { // 枚举每一组
for (int j = W; j >= 0; j--) { // 枚举容量
for (int k = 0; k < w[i].size(); k++) { // 枚举组内物品
if (j >= w[i][k]) {
dp[j] = max(dp[j], dp[j-w[i][k]] + v[i][k]);
}
}
}
}
return dp[W];
}
Java 实现
class Solution {
public int groupKnapsack(int[][] w, int[][] v, int W) {
int n = w.length; // 组数
int[] dp = new int[W + 1];
for (int i = 0; i < n; i++) {
for (int j = W; j >= 0; j--) {
for (int k = 0; k < w[i].length; k++) {
if (j >= w[i][k]) {
dp[j] = Math.max(dp[j], dp[j-w[i][k]] + v[i][k]);
}
}
}
}
return dp[W];
}
}
Python 实现
def groupKnapsack(w: List[List[int]], v: List[List[int]], W: int) -> int:
n = len(w) # 组数
dp = [0] * (W + 1)
for i in range(n): # 枚举每一组
for j in range(W, -1, -1): # 枚举容量
for k in range(len(w[i])): # 枚举组内物品
if j >= w[i][k]:
dp[j] = max(dp[j], dp[j-w[i][k]] + v[i][k])
return dp[W]
时间复杂度分析
- 时间复杂度:O(n⋅W⋅k)\mathcal{O}(n \cdot W \cdot k)O(n⋅W⋅k),其中 kkk 是每组物品的平均数量
- 空间复杂度:O(W)\mathcal{O}(W)O(W)
优化方法
-
预处理优化:
- 预先去除组内被其他物品完全支配的物品
- 支配关系:重量小价值大的物品支配重量大价值小的物品
-
记忆化搜索:
- 对于组数较少但每组物品较多的情况
- 可以使用记忆化搜索优化
应用场景
- 套装选择问题
- 课程选修规划
- 技能树培养
- 装备搭配优化
- 团队人员选择
变体问题
- 有依赖的分组背包
- 多维费用的分组背包
- 有公共物品的分组背包
- 求方案数的分组背包
注意事项
- 组内物品的处理
- 容量的遍历顺序
- 状态转移的正确性
- 内存使用限制
- 特殊情况处理