分组背包
每组至多选一件
本课摘要
分组背包课程回答“每组至多选一件时,如何隔离组内选择”。内容以每组至多选一件为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。
- 判断分组背包的适用条件与状态边界
- 围绕“每组至多选一件”推导转移与计算顺序
- 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节
正在整理目录…
当物品被「分了组」
先看一个具体场景:有 2 组物品,一个容量 的背包。组 1 里放着两件,组 2 里放着两件 。规则多了一条硬约束,每一组里至多挑一件(也可以一件都不挑),组与组之间互不影响。目标仍是不超重的前提下让总价值最大。
为什么不能把它当普通 01 背包,把 4 件一股脑丢进去做?因为 01 背包允许「组 1 的两件同时拿」, 重 5、价值 7,它会毫不犹豫地收下。可分组规则里这是非法的:同一组内互斥。 普通 01 背包压根不知道「组」的存在,自然管不住「一组只能出一件」。
那把每组「挑哪一件、或不挑」的所有搭配枚举出来呢? 组、每组约 种选择,就是 种组合, 又回到指数爆炸。分组背包的思路,是把这层组内的互斥,直接焊进背包的转移里,让「组」成为 DP 的阶段。
状态与转移:以「组」为阶段
定状态。设 表示:只在前 组里挑选(每组至多一件)、总重量不超过 时的最大价值。 和 01 背包最大的不同在阶段的粒度:01 里一个阶段决断「第 件取不取」,分组里一个阶段决断「第 组,不选,还是选组内的哪一件」。
不选第 组:本组一件都不拿,前 组的最优就等于前 组在同容量 下的最优,即 。
选第 组里的某一件 (需装得下 ):腾出 ,剩下的 留给前 组去最优,再加上这件的价值 ,即 。 究竟选组内哪一件?把每一件都试一遍,取最好的那件。
合起来,就是转移方程,注意第二项里那个对组内物品的 :
边界:(一组都不考虑,价值为 0)。答案:。 对比 01 背包 ,分组只是把「取这一件」换成了「在组内挑最好的一件」,多套了一层组内的 。
本质
分组背包是 01 背包的自然推广:把决策的粒度从「一件」抬升到「一组」。两项候选都从上一行 取值,这一句就锁死了「每组至多一件」:因为一件都还没往本行写,组内不管试多少件,用的都是「本组尚未出手」的旧值。
跟着算一遍
用开头的例子(组 1 = ,组 2 = ,容量 6)走几步,把方程「跑起来」,重点盯住每组只出一件:
看它一格一格长出来
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
压成一维:三重循环,与那道循环顺序的坎
和 01 背包一样,转移只用到上一行 ,于是可以卷成一维 就地更新。 但组内多了一层枚举,一维写法的循环套三层,顺序有讲究:
for 组 g = 1 … G:
for j = m downto 0: // ★容量倒序,在组内枚举之外
for 组内每件 (w, v):
f[j] = max( f[j], f[j − w] + v )记住这个骨架的关键:容量循环 必须在「组内物品枚举」的外层,而且照旧倒序。 这样一来,处理组 时,无论组内枚举到第几件, 用的都是本组还没动过的旧值(即上一行的值),组内各件都在「本组尚未出手」的同一起点上竞争,自然只会有一件胜出被计入。
若把容量循环放进组内,会怎样
把三重循环写反,让组内物品在外、容量 在里:
for 组 g = 1 … G:
for 组内每件 (w, v): // ✗ 组内枚举跑到了外层
for j = m downto 0:
f[j] = max( f[j], f[j − w] + v )这时组内每一件都各自独立地跑一遍完整的倒序背包。第一件更新完 后,第二件是在「第一件已经装进去」的结果上继续做,于是同一组的两件可以被同时选中。 这恰好退化成「把这一组当作若干件各自独立的 01 物品」,组内互斥的约束彻底失效。
用开头组 1 、容量 5 验一下错法:先跑 得 ;再跑 时 , 里已经含了 ,于是 7 = 两件相加。可正确答案(组内至多一件)只该是 。一层循环放错位置,答案就从 4 涨成了 7。
记死:容量循环夹在「组」与「组内件」之间
三重循环的正序是 组 → 容量(倒序) → 组内件。容量循环既不能提到最外(那样组与组之间会串味),也不能沉到最里(那样组内会多选)。它必须正好夹在中间。这和 01 背包「必须倒序」是同一个「用干净旧值」的道理,只是把粒度从「每件一次」升到了「每组一次」。
并排看:一层循环放错,答案就涨了
道理讲完,不如让两种顺序同跑一遍并排对照。默认就是本节手算的那组:单独一组 、容量 5。 左边把容量倒序放在组内件之外,组内两件都基于「本组未动过」的旧值竞争,只有一件胜出,; 右边把容量倒序沉进组内件里层,第二件在「第一件已装进去」的结果上继续叠,两件被同时计入,。 单步走到右侧 那一格,会看到来源列被标红:那正是「同组两件叠在一起」的瞬间。改改 w / v 或再加一组,看这 与 的差随之变化。
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
例题
#include <iostream>
#include <algorithm>
using namespace std;
int w[3405], v[3405], g[3405]; // 重量、价值、所在组号
int f[3405]; // f[j]:容量不超过 j 的最大价值
int idx[105][3405], cnt[105]; // 每组归集:idx[组][第几件] = 物品下标
int mx; // 出现过的最大组号
int main()
{
int m, n;
cin >> m >> n;
for (int i = 1; i <= n; i++)
{
cin >> w[i] >> v[i] >> g[i];
idx[g[i]][++cnt[g[i]]] = i; // 把第 i 件挂到它所在的组
mx = max(mx, g[i]);
}
for (int t = 1; t <= mx; t++) // ★外层:逐组
for (int j = m; j >= 0; j--) // ★中层:容量倒序(在组内物品之外)
for (int k = 1; k <= cnt[t]; k++) // ★内层:枚举本组每一件
{
int i = idx[t][k];
if (j >= w[i])
f[j] = max(f[j], f[j - w[i]] + v[i]);
}
cout << f[m] << endl;
return 0;
}练习
说明:纯分组背包的洛谷原生题目池较窄,更多「组内互斥」的进阶练习并入 有依赖的背包 与 F 树上背包。下面两题分别从「依赖归约」与「裸模板复现」两头夯实基础。

