A背包 DP

分组背包

每组至多选一件

本课摘要

分组背包课程回答“每组至多选一件时,如何隔离组内选择”。内容以每组至多选一件为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。

  • 判断分组背包的适用条件与状态边界
  • 围绕“每组至多选一件”推导转移与计算顺序
  • 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节

正在整理目录…

当物品被「分了组」

先看一个具体场景:有 2 组物品,一个容量 m=6m=6 的背包。组 1 里放着两件(w,v)=(2,3),(3,4)(w,v)=(2,3),(3,4),组 2 里放着两件 (2,2),(4,5)(2,2),(4,5)。规则多了一条硬约束,每一组里至多挑一件(也可以一件都不挑),组与组之间互不影响。目标仍是不超重的前提下让总价值最大。

组 1第 1 件w=2v=3第 2 件w=3v=4组内至多挑 1 件组 2第 1 件w=2v=2第 2 件w=4v=5组内至多挑 1 件
2 组物品,组内互斥:每组至多取一件,组 1 里 (2,3) 与 (3,4) 只能二选一或都不选。

为什么不能把它当普通 01 背包,把 4 件一股脑丢进去做?因为 01 背包允许「组 1 的两件同时拿」,(2,3)+(3,4)(2,3)+(3,4) 重 5、价值 7,它会毫不犹豫地收下。可分组规则里这是非法的:同一组内互斥。 普通 01 背包压根不知道「组」的存在,自然管不住「一组只能出一件」。

那把每组「挑哪一件、或不挑」的所有搭配枚举出来呢?gg 组、每组约 cc 种选择,就是 cgc^g 种组合, 又回到指数爆炸。分组背包的思路,是把这层组内的互斥,直接焊进背包的转移里,让「组」成为 DP 的阶段。

状态与转移:以「组」为阶段

定状态。设 f[g][j]f[g][j] 表示:只在前 gg 组里挑选(每组至多一件)、总重量不超过 jj 时的最大价值。 和 01 背包最大的不同在阶段的粒度:01 里一个阶段决断「第 ii 件取不取」,分组里一个阶段决断「第 gg 组,不选,还是选组内的哪一件」。

第 g 组 · 容量 jf[g][j] = ?不选本组选组内某一件本组一件都不拿= f[g−1][j]枚举组内第 k 件,取 max= f[g−1][j−wₖ] + vₖ取较大者 = max(两者)两条路都只回看上一行 f[g−1][·],所以本组至多贡献一件
每格 f[g][j] 有两条路:不选本组,继承上一行;或在组内枚举第 k 件取 max。两条路都只回看上一行,本组至多出一件。

不选第 gg 组:本组一件都不拿,前 gg 组的最优就等于前 g−1g-1 组在同容量 jj 下的最优,即 f[g−1][j]f[g-1][j]。

选第 gg 组里的某一件 kk(需装得下 j≥wkj\ge w_k):腾出 wkw_k,剩下的 j−wkj-w_k 留给前 g−1g-1 组去最优,再加上这件的价值 vkv_k,即 f[g−1][j−wk]+vkf[g-1][j-w_k]+v_k。 究竟选组内哪一件?把每一件都试一遍,取最好的那件。

合起来,就是转移方程,注意第二项里那个对组内物品的 max⁡\max:

f[g][j]=max⁡( f[g−1][j], max⁡k ∈ g, wk≤j(f[g−1][j−wk]+vk))f[g][j]=\max\Big(\,f[g-1][j],\ \max_{k\,\in\,g,\ w_k\le j}\big(f[g-1][j-w_k]+v_k\big)\Big)

边界:f[0][j]=0f[0][j]=0(一组都不考虑,价值为 0)。答案:f[G][m]f[G][m]。 对比 01 背包 f[i][j]=max⁡(f[i−1][j], f[i−1][j−wi]+vi)f[i][j]=\max(f[i-1][j],\ f[i-1][j-w_i]+v_i),分组只是把「取这一件」换成了「在组内挑最好的一件」,多套了一层组内的 max⁡\max。

本质

分组背包是 01 背包的自然推广:把决策的粒度从「一件」抬升到「一组」。两项候选都从上一行 f[g−1][⋅]f[g-1][\cdot] 取值,这一句就锁死了「每组至多一件」:因为一件都还没往本行写,组内不管试多少件,用的都是「本组尚未出手」的旧值。

跟着算一遍

用开头的例子(组 1 = (2,3),(3,4)(2,3),(3,4),组 2 = (2,2),(4,5)(2,2),(4,5),容量 6)走几步,把方程「跑起来」,重点盯住每组只出一件:

0
初始化第 0 行。 一组都不考虑,任何容量下价值都是 0:f[0][0..6]=0f[0][0..6]=0。整张表的地基。
1
处理组 1(含 (2,3),(3,4)(2,3),(3,4))。看容量 5:不选本组 = f[0][5]=0f[0][5]=0;选 (2,3)(2,3) = f[0][3]+3=3f[0][3]+3=3;选 (3,4)(3,4) = f[0][2]+4=4f[0][2]+4=4。三者取最大 → f[1][5]=4f[1][5]=4。第 1 行整体为 0,0,3,4,4,4,40,0,3,4,4,4,4。
2
处理组 2(含 (2,2),(4,5)(2,2),(4,5)),看容量 6:不选本组 = f[1][6]=4f[1][6]=4;选 (2,2)(2,2) = f[1][4]+2=4+2=6f[1][4]+2=4+2=6;选 (4,5)(4,5) = f[1][2]+5=3+5=8f[1][2]+5=3+5=8。取最大 → f[2][6]=8f[2][6]=8。
3
读答案。 f[2][6]=8f[2][6]=8,它来自「组 1 选 (2,3)(2,3) + 组 2 选 (4,5)(4,5)」,重 2+4=62+4=6、价值 3+5=83+5=8。每组恰好一件,正是分组规则下的最优。
下面的演示会把整张表逐格填满,高亮每格「跳过本组」与「选组内某件」两个来源。改改组、件或容量,看表实时重算。

看它一格一格长出来

分组(每组内至多选一件 · 可改 w / v)
组 1
1
重量 w
2
价值 v
3
2
重量 w
3
价值 v
4
组 2
1
重量 w
2
价值 v
2
2
重量 w
4
价值 v
5
背包容量
m
6
0
1
2
3
4
5
6
∅
组1
组2
0
0
0
0
0
0
0
·
·
·
·
·
·
·
·
·
·
·
·
·
·
当前计算 依赖来源 被选转移 已确定
f[0][j]=0f[0][j] = 0
第 0 行:一组都不考虑时,任何容量下最大价值都是 0(初始化的地基)。
已暂停,第 1 步,共 16 步,1 倍速

压成一维:三重循环,与那道循环顺序的坎

和 01 背包一样,转移只用到上一行 f[g−1][⋅]f[g-1][\cdot],于是可以卷成一维 f[j]f[j] 就地更新。 但组内多了一层枚举,一维写法的循环套三层,顺序有讲究:

for 组 g = 1 … G:
  for j = m downto 0:        // ★容量倒序,在组内枚举之外
    for 组内每件 (w, v):
      f[j] = max( f[j], f[j − w] + v )

记住这个骨架的关键:容量循环 jj 必须在「组内物品枚举」的外层,而且照旧倒序。 这样一来,处理组 gg 时,无论组内枚举到第几件,f[j−w]f[j-w] 用的都是本组还没动过的旧值(即上一行的值),组内各件都在「本组尚未出手」的同一起点上竞争,自然只会有一件胜出被计入。

✓ 容量在组内物品之外for 组 g:for j = m…w: (倒序)for 组内每件 (w,v):f[j]=max(f[j],f[j−w]+v)一组内各件都基于旧值 → 至多选 1 件✗ 容量在组内物品之内for 组 g:for 组内每件 (w,v):for j = m…w: (倒序)f[j]=max(f[j],f[j−w]+v)前一件已更新 f → 同组可再选 → 退化
左:容量 j 在组内物品之外,组内各件都基于旧值,每组至多选 1 件(正确)。右:容量 j 被塞进组内物品里层,前一件已改 f[j],同组下一件又叠上去,一组能选出多件,退化成「组内可重复取」(错误)。

若把容量循环放进组内,会怎样

把三重循环写反,让组内物品在外、容量 jj 在里:

for 组 g = 1 … G:
  for 组内每件 (w, v):        // ✗ 组内枚举跑到了外层
    for j = m downto 0:
      f[j] = max( f[j], f[j − w] + v )

这时组内每一件都各自独立地跑一遍完整的倒序背包。第一件更新完 f[⋅]f[\cdot] 后,第二件是在「第一件已经装进去」的结果上继续做,于是同一组的两件可以被同时选中。 这恰好退化成「把这一组当作若干件各自独立的 01 物品」,组内互斥的约束彻底失效。

用开头组 1 (2,3),(3,4)(2,3),(3,4)、容量 5 验一下错法:先跑 (2,3)(2,3) 得 f[5]=3f[5]=3;再跑 (3,4)(3,4) 时 f[5]=max⁡(3, f[2]+4)=max⁡(3,3+4)=7f[5]=\max(3,\ f[2]+4)=\max(3,3+4)=7,f[2]=3f[2]=3 里已经含了 (2,3)(2,3),于是 7 = 两件相加。可正确答案(组内至多一件)只该是 44。一层循环放错位置,答案就从 4 涨成了 7。

记死:容量循环夹在「组」与「组内件」之间

三重循环的正序是 组 → 容量(倒序) → 组内件。容量循环既不能提到最外(那样组与组之间会串味),也不能沉到最里(那样组内会多选)。它必须正好夹在中间。这和 01 背包「必须倒序」是同一个「用干净旧值」的道理,只是把粒度从「每件一次」升到了「每组一次」。

并排看:一层循环放错,答案就涨了

道理讲完,不如让两种顺序同跑一遍并排对照。默认就是本节手算的那组:单独一组 (2,3),(3,4)(2,3),(3,4)、容量 5。 左边把容量倒序放在组内件之外,组内两件都基于「本组未动过」的旧值竞争,只有一件胜出,f[5]=4f[5]=4; 右边把容量倒序沉进组内件里层,第二件在「第一件已装进去」的结果上继续叠,两件被同时计入,f[5]=7f[5]=7。 单步走到右侧 j=5j=5 那一格,会看到来源列被标红:那正是「同组两件叠在一起」的瞬间。改改 w / v 或再加一组,看这 44 与 77 的差随之变化。

分组(组内至多一件 · 可改 w / v · 默认一组,可再加)
组 1
1
重量 w
2
价值 v
3
2
重量 w
3
价值 v
4
背包容量
m
5
正确顺序 f[5] = 4(每组至多一件) · 错误顺序 f[5] = 7(错法把同组多件重复计入,答案被抬高了 3)
容量倒序在组内件之外 · 正确(每组至多一件)
0
1
2
3
4
5
f
0
0
0
0
0
0
当前计算 依赖来源 被选转移 已确定
f[j]=0f[j]=0
初始:容量 0…W 的最大价值都是 0(空背包)。
已暂停,第 1 步,共 8 步,1 倍速
容量倒序沉进组内件里层 · 错误(同组多件被叠加)
0
1
2
3
4
5
f
0
0
0
0
0
0
当前计算 依赖来源 被选转移 已确定
f[j]=0f[j]=0
初始:容量 0…W 的最大价值都是 0(空背包)。
已暂停,第 1 步,共 9 步,1 倍速

例题

P1757通天之分组背包洛谷原生普及/提高-
题意
背包容量 mm,nn 件物品,每件给出重量 aia_i、价值 bib_i 和所在组号 cic_i。同一组至多取一件,求最大价值。
为什么选它
分组背包最纯净的裸模板:读入后按组号归集,直接套三重循环骨架。没有任何抽象包装,是把「组 → 容量倒序 → 组内件」这个顺序肌肉记忆下来的最佳一题。
转移 · 复杂度
f[j]=max⁡(f[j], f[j−ai]+bi)f[j]=\max(f[j],\ f[j-a_i]+b_i),外层枚举组、中层 jj 倒序、内层枚举组内件;时间 O(nm)O(nm)。
参考代码(标准三重循环)
#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;
}
P5322[BJOI2019] 排兵布阵BJOI2019提高+/省选-
题意
SS 位对手、nn 座城池、你有 mm 名士兵,要把兵力分配到各城。在某城派出严格多于对手 2 倍的兵力即击败该对手,击败第 ii 城的对手得 ii 分(对每位对手分别结算)。求最高总分。
状态设计(把城池抽象成组)
每座城池 = 一组。把该城 SS 位对手在此城的守军从小到大排序记为 c1≤c2≤⋯≤cSc_1\le c_2\le\dots\le c_S,则「同时击败守军最少的前 kk 名对手」构成组内第 kk 件物品:击败一名需严格多于其守军 2 倍,同时击败前 kk 名只需压过其中门槛最高的一位,故体积 = 2ck+12c_k+1(排序后 ckc_k 即前 kk 名里的最大守军),价值 = k×ik\times i(击败 kk 名、城池编号 ii)。一组内至多选一件,恰好对应「在这座城要么不争、要么争到前 kk 名」。
为什么选它
较新的省选题,示范分组背包的建模功夫:真正的难点不是转移,而是看出「城池是组、击败前 k 名是组内物品」。转移仍是标准三重循环骨架(见 P1757),代码只需换掉组内物品的「体积/价值」定义。

练习

说明:纯分组背包的洛谷原生题目池较窄,更多「组内互斥」的进阶练习并入 有依赖的背包 与 F 树上背包。下面两题分别从「依赖归约」与「裸模板复现」两头夯实基础。

P1064[NOIP2006 提高组] 金明的预算方案主件-附件的依赖可归约为分组背包:把「一个主件 + 它的若干附件」的所有合法组合(仅主 / 主+附1 / 主+附2 / 主+附1+2)打包成同一组的组内物品,组内至多选一件。也属有依赖背包,做承接。在洛谷打开
P1757通天之分组背包学完回来独立复现三重循环骨架:外层组、中层容量倒序、内层组内件。不看题解默写一遍,巩固「组内至多一件」为何要靠循环顺序保证。在洛谷打开
到 A 部分页的「装包大师」挑物品时留意:若把清单按「同一栏里只能拿一件」重新分栏,你面对的就是分组背包,组内互斥,正是它区别于 01 背包的那一笔。

已进入 分组背包 · 背包 DP · DP大师