分组背包变式:每组最多选 $d_i$ 件
本篇对应源码见仓库
solutions/xcpc/algorithm/dp/knapsack-group/。
本篇从思想到代码,分步讲解「每组最多选 \(d_i\) 件物品」的分组背包如何做,然后给出两个示例习题(一个手写小例子+一个经典“选课问题”)进行巩固。
一、问题抽象与 DP 状态
-
全局约束
- 容量上限:背包总容量为 \(W\)。
-
分组约束
-
物品被分成 \(m\) 组,第 \(i\) 组有若干件物品,每件有重量 \(w_{i,t}\)、价值 \(v_{i,t}\)。
-
每组最多可以选 \(d_i\) 件物品。
-
我们用 \(\mathrm{dp}[j]\) 表示当前处理完若干组后,在容量恰好为 \(j\) 时能取得的最大价值。
为处理组内「最多选 \(d_i\) 件」的约束,我们引入组内的临时二维数组
\(\mathrm{cur}[j][k]\)
— 表示在“当前组”中,已用容量 \(j\),且已选了 \(k\) 件(\(0\le k\le d_i\))时,能在“前面各组”基础上再能获得的最大附加价值。
组间转移大框架
初始化:dp[j](处理完第 i−1 组后的结果)
—— 处理第 i 组 ——
1. 构造 cur[j][k],并设所有 cur[j][k]=-∞,再令 cur[j][0]=dp[j]
2. 枚举本组每件物品 (w,v):
∀ j=W…w ↓, ∀ k=1…d_i ↑:
cur[j][k] = max(cur[j][k], cur[j−w][k−1] + v)
3. 合并到全局:
∀ j=0…W:
dp[j] = max_{0≤k≤d_i} cur[j][k]
-
步骤 1:把“选 0 件” 时的价值先搬进来。
-
步骤 2:相当于在组内做一次「0-1 背包 + 计数限制」的 DP。
-
步骤 3:枚举该组选 \(k\) 件的所有可能性,把最优值更新回去。
二、C++ 代码模板
#include <bits/stdc++.h>
using namespace std;
const int INF = 1e9;
struct Item { int w, v; };
int main(){
int m, W;
// m 组,W 容量
cin >> m >> W;
vector<vector<Item>> groups(m);
vector<int> d(m);
for(int i = 0; i < m; i++){
int cnt;
cin >> cnt >> d[i];
// cnt: 第 i 组物品数, d[i]: 最多选件数
groups[i].resize(cnt);
for(int t = 0; t < cnt; t++){
cin >> groups[i][t].w >> groups[i][t].v;
}
}
vector<int> dp(W+1, 0);
// 组间循环
for(int i = 0; i < m; i++){
// cur[j][k]: 本组容量 j,选了 k 件时的附加价值
vector<vector<int>> cur(W+1, vector<int>(d[i]+1, -INF));
// 选 0 件 -> 继承上一组结果
for(int j = 0; j <= W; j++)
cur[j][0] = dp[j];
// 组内枚举每件物品(0-1)
for(auto &it : groups[i]){
int wt = it.w, vt = it.v;
for(int j = W; j >= wt; j--){
for(int k = 1; k <= d[i]; k++){
if(cur[j-wt][k-1] > -INF)
cur[j][k] = max(cur[j][k], cur[j-wt][k-1] + vt);
}
}
}
// 合并回 dp
for(int j = 0; j <= W; j++){
int best = cur[j][0];
for(int k = 1; k <= d[i]; k++)
best = max(best, cur[j][k]);
dp[j] = best;
}
}
// 答案:恰好装满 W 时的最大价值
cout << dp[W] << "\n";
return 0;
}
三、示例 1:手写小例子
-
容量 \(W=5\)
-
一组,最多选 \(d_0=2\) 件
-
物品列表:
-
重量 2,价值 3
-
重量 3,价值 4
-
重量 4,价值 5
-
手算思路:
| 选 0 件 | 选 1 件 | 选 2 件 |
|---|---|---|
| 重量 0,价值 0 | (2,3)、(3,4)、(4,5) 三种 | (2+3, 3+4)= (5,7) |
-
容量 5 时,
-
选 0 件 → 价值 0
-
选 1 件 → 最优是选(4,5) → 5
-
选 2 件 → 只能选(2,3)+(3,4) → 7
-
-
最终答案 = \(\max(0,5,7)=\boxed{7}\)。
四、示例 2:NOIP2012 提高组「选课问题」
题目
某校要从 \(m\) 个学期的若干门课程中选课。第 \(i\) 学期有 \(c_i\) 门可选课程,每门课程占用学分 \(w_{i,t}\),学习收益 \(v_{i,t}\)。
全校总学分限额不超过 \(W\),并且每学期最多只能选 \(K\) 门课程。求最高学习收益。
-
这是上述模型的典型应用:
-
组数 \(m\)
-
容量 \(W\)
-
每组物品数 \(c_i\)
-
每组限选件数 \(d_i\equiv K\)
-
只要把上面模板中 d[i] 都设为 \(K\),再读入各学期的课程列表,就能打出答案。
// 读入 m, W, K
int m, W, K;
cin >> m >> W >> K;
vector<vector<Item>> course(m);
vector<int> d(m, K);
for(int i = 0; i < m; i++){
int ci; cin >> ci;
course[i].resize(ci);
for(int j = 0; j < ci; j++){
cin >> course[i][j].w >> course[i][j].v;
}
}
// 接下来用上面“组间、组内 DP”模板即可
小结
-
多选限制:引入“组内已选件数”维度 \(k\)。
-
组内 DP:对每组做一次「带计数的 0-1 背包」。
-
组间合并:枚举选 \(k\le d_i\) 件后的最优值,更新全局。
这样,任何「每组最多选 \(d_i\) 件」的变形,都能被同一个框架解决。希望通过这两道练习,你能彻底搞懂并灵活运用!如果还有疑问或想要更多变形题,随时告诉我。
为什么 \(cur\) 初始化为-INF?而不是0?
在组内 DP 里,cur[j][k] 的含义是“在前面组的基础上,当前组恰好用 j 容量且选了 k 件物品时,能额外拿到的最大价值”。
-
标记「不可达」状态
-
对于
k>0而还没放任何物品的情况,显然是不可达的——你不可能在本组里选 1 件但又没放任何东西。 -
我们用一个非常小的数(
-INF)来表示“这个状态根本不可取”;在后续的取max()时,不可达状态就永远不会胜出。
-
-
如果初始化成 0 会怎样?
- 那么所有
(j,k)都至少是 0,就算是根本不可能选出的组合,也会被误当成“选了几件但价值为 0”──这会让后面合并回全局dp时,把「不合法的选项」当成合法选项,污染答案。
- 那么所有
-
cur[j][0]例外- 选 0 件时是合法的:
cur[j][0] = dp[j],表示「这一组不拿任何东西」,直接继承上次组间的最优。
- 选 0 件时是合法的:
简而言之,-INF 就是告诉算法“这里根本到达不了”,保证只有真正可行的“放了哪些物品”组合才会参与到后面的取优里。

浙公网安备 33010602011771号