分组背包
分组背包
一、问题描述
有 件物品,分成 组,背包容量为 。第 件物品的重量为 ,价值为 。
每组最多选择一件物品,也可以不选。 求总重量不超过 时,能够得到的最大总价值。
例题:P1308 分组背包。
题目样例中,背包容量为 ,物品分为三组:
| 组别 | 组内物品(重量,价值) |
|---|---|
| 1 | 、 |
| 2 | 、 |
| 3 | 、 |
分别选择 、、,总重量为 ,总价值为 ,这是最优方案。
01 背包限制每件物品最多选一次;分组背包还要求:同一组内的不同物品不能同时选择。
二、状态与转移
设 表示:
只考虑前 组物品,总重量不超过 时,能够得到的最大总价值。
考虑第 组,有两类选择:
- 不选这一组,价值为 。
- 选择组内某件物品 ,价值为 ,要求 。
设 为第 组的物品集合,则:
$$\boxed{ dp[i][j]=\max\left( dp[i-1][j],\quad \max_{\substack{k\in G_i\\w[k]\le j}} \bigl(dp[i-1][j-w[k]]+v[k]\bigr) \right). }$$如果组内没有能放下的物品,只保留不选这一组的情况。
所有选择都从第 组的状态转移。 来源中没有第 组的物品,加上当前物品后,就能保证这一组最多选一件。
初始时没有物品可选,因此 。最终答案为 。
三、一维优化:改变枚举顺序
用一维数组 dp[j] 保存状态,循环顺序为:
枚举组别 → 倒序枚举容量 → 枚举组内物品。
固定容量 ,枚举这一组选哪件物品,取最大值后再更新 dp[j]:
for (int i = 1; i <= T; ++i) {
for (int j = V; j >= 0; --j) {
int best = dp[j]; // 不选第 i 组
for (int k : group[i]) {
if (j >= w[k]) {
best = max(best, dp[j - w[k]] + v[k]);
}
}
dp[j] = best;
}
}
当 时,。由于容量倒序枚举,较小容量还没有在本轮更新,因此 dp[j-w[k]] 保存的是上一组的状态。
容量循环必须放在组内物品循环的外层。 如果对组内每件物品分别做一次普通的 01 背包,仍可能同时选中同组的多件物品。例如,一组中只有 和 ,容量为 时,答案应该是 ,不能把两件都选上得到 。
为什么这里使用 best?
如果所有物品的重量都大于 ,可以省去 best。 保持上述循环顺序,在组内直接更新即可:
dp[j] = max(dp[j], dp[j - w[k]] + v[k]);
因为来源容量严格小于 ,更新 dp[j] 不会影响组内其他物品的转移来源。
但本题允许 ,此时来源就是 dp[j]。例如,同组有两件重量为 、价值为 的物品,初始 dp[j]=0,直接更新会让 dp[j] 先变成 ,再变成 ,错误地选择了同组的两件物品。
因此,本题参考代码使用临时变量 best 记录最大值,组内枚举结束后才写回 dp[j],保证所有候选都读取上一组的状态。best 用于处理零重量这一边界情况,并非所有分组背包题都必须使用。
四、参考代码
例题输入第一行为 ,接下来 行,每行输入一件物品的重量、价值和组别,即 。代码中用 w[k]、v[k] 保存重量和价值,用 group[p] 保存第 组的物品编号。
数据范围:、、,、、。
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int MAX_N = 55;
const int MAX_V = 205;
const int MAX_T = 15;
int V, n, T;
int w[MAX_N], v[MAX_N];
vector<int> group[MAX_T];
int dp[MAX_V];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> V >> n >> T;
for (int k = 1; k <= n; ++k) {
int p;
cin >> w[k] >> v[k] >> p;
group[p].push_back(k);
}
for (int i = 1; i <= T; ++i) {
for (int j = V; j >= 0; --j) {
int best = dp[j]; // 不选第 i 组
for (int k : group[i]) {
if (j >= w[k]) {
best = max(best, dp[j - w[k]] + v[k]);
}
}
dp[j] = best;
}
}
cout << dp[V] << '\n';
return 0;
}
全局数组初始值为 ,对应允许不选任何物品。最多选择 件物品,每件价值最多为 ,因此 int 足以保存答案。
五、复杂度
每组枚举 个容量,每件物品在每个容量下考虑一次,时间复杂度为 。
DP 数组占用 空间,加上物品和分组信息,总空间复杂度为 。