有依赖的背包
主件-附件·依赖→分组
本课摘要
有依赖的背包课程回答“带主件附件依赖的选择怎样转化为合法组合”。内容以主件-附件·依赖→分组为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。
- 判断有依赖的背包的适用条件与状态边界
- 围绕“主件-附件·依赖→分组”推导转移与计算顺序
- 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节
正在整理目录…
当「选它」得先「选它的主件」
先看一个具体场景:有一件主件 ,还有它的两件附件, 附件 1 、附件 2 ,一个容量 的背包。规则多了一条硬约束:附件必须依附主件而选,想装附件 1,就必须先把主件也装上;主件不装,两个附件都是非法的。目标仍是不超重下让总价值最大。
很自然会想:把主件、附件 1、附件 2 当成三件独立的 01 物品丢进背包不就行了?不行。普通 01 背包会毫不客气地只挑附件 1、不挑主件(附件 1 性价比高),可这在依赖规则里是非法的,它压根不知道「附件得先有主件」这回事。 反过来,也没法用「先强制装主件再随便挑附件」蒙混:主件到底装不装、装了之后还剩多少钱给附件,本身就是要一起权衡的决策。
那把「主件带哪些附件」的所有情形枚举出来呢?主件要么不装;一旦装,它的两个附件各可带可不带,仅主 / 主+附1 / 主+附2 / 主+附1+2,加上「整个不装」,就把这一族物品的合法方案全数罗列了。这份枚举,正是打开依赖背包的钥匙。
归约:把「主 + 附件子集」打包成组
盯住上面那句枚举。一个主件带上它附件的某个子集,就构成一个合法组合;每个组合的费用 = 主件费用 + 所选附件费用之和,价值 = 各自价值之和。 本例主件 配两个附件,附件子集有 种,于是得到 4 个组合:
关键的一跃在这里:这 4 个组合互斥,你不可能同时「只带附件 1」又「两个附件都带」,一个主件最终只能落实成其中一种方案(或整个不选)。这正是分组背包的定义:把这些组合归为同一组,组内至多选一个。
于是有依赖的背包,被归约成了分组背包,一个主件(连同它的附件)= 一组,组内物品 = 该主件的各个合法组合。为什么必须走这条「枚举组合」的路、而不能把附件当独立物品?因为独立物品会漏掉「选附件必先选主件」这条约束;而把主件焊进每个组合里,就让「带附件」永远伴随「带主件」,约束天然成立。
状态与转移:落到分组背包
既然归约成了分组背包,转移就直接套分组背包那一套。设 为花费不超过 时的最大价值,逐个主件(每个当一组)更新。 处理某个主件这一组时,一格 有两条来路:
不选本组:这个主件连同附件一概不要,价值就是处理本组之前的 。
选组内某个组合 (费用 、价值 ,需 ):腾出 ,剩下的 交给之前的最优,再加上 。究竟选哪个组合?把每个组合都试一遍取最好。合起来就是分组背包的转移:
一维写法照分组背包:外层枚举主件(组)、中层容量 倒序、内层枚举本组的各个组合。容量倒序保证组内各组合都基于「本组尚未出手」的旧值,一组至多落实一个组合,正好对应「一个主件最终只有一种方案」。
本质
有依赖的背包 = 分组背包的一个实例。诀窍全在建组:把「一个主件 + 它附件的任一子集」枚举成组内物品,用「组合恒含主件」把「选附件必先选主件」这条依赖,化进了物品的定义里。归约完成后,转移与循环顺序一字不差地照搬分组背包。
跟着算一遍
用本例(主件 、附件 与 ,容量 7)走一遍,重点盯住枚举组合 → 组内取一个:
看它把依赖枚成组、再逐格转移
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
深化:附件多了、依赖连成了树
本例每个主件只挂 2 个附件,枚举 个组合毫无压力。P1064 正是这种「主件 + 至多 2 附件」的教科书原型,每个主件最多 4 个组合,直接枚举即可。 但依赖可以更深:如果附件本身又能挂自己的附件,依赖关系就从「主-附两层」长成了一棵树(甚至一片森林)。
P2014 选课就是这样:一门课可能有先修课,要选它必先选先修,先修关系把课程连成树。这时「枚举一个节点的所有后代子集」会指数爆炸,不能再照搬本页的暴力枚举,而要在树上做 DP: 表示以 为根的子树、选课数(或容量)为 时的最优,把子树当分组、在各子树间做背包合并。
承接:依赖成树 → 树上背包
「主件-附件」是依赖背包最浅的两层形态,归约成分组背包即可解。当依赖连成树/森林(如 P2014 选课的先修关系),它一般化为树上背包(树形 DP),那是F 部分的主题。本页只点到「依赖成树」这一形态,树形转移的细节留到那里展开。
例题
#include <iostream>
#include <algorithm>
using namespace std;
int mw[65], mv[65]; // 主件的费用、价值(=价格×重要度)
int aw[65][3], av[65][3]; // 每个主件的附件(至多 2 个):费用、价值
int cnt[65]; // 每个主件挂了几个附件
long long f[32005]; // f[j]:花费不超过 j 时的最大 价格×重要度 之和
int main()
{
int n, m;
cin >> n >> m; // n=总钱数,m=物品数
for (int i = 1; i <= m; i++)
{
int v, p, q;
cin >> v >> p >> q; // v=价格, p=重要度(1~5), q=0 主件 / 否则=所属主件编号
if (q == 0) // 是主件
{
mw[i] = v;
mv[i] = v * p;
}
else // 是附件,挂到主件 q 上
{
aw[q][cnt[q]] = v;
av[q][cnt[q]] = v * p;
cnt[q]++;
}
}
for (int i = 1; i <= m; i++) // 逐个主件,当作「一组」
{
if (mw[i] == 0) continue; // i 不是主件(是附件或不存在),跳过
for (int j = n; j >= mw[i]; j--) // ★倒序:组内至多选一个组合
{
// 枚举本主件的合法组合(含主件),对附件的每个子集取一遍
for (int s = 0; s < (1 << cnt[i]); s++)
{
int w = mw[i], val = mv[i]; // 组合恒含主件
for (int k = 0; k < cnt[i]; k++)
if (s >> k & 1) // 该附件入选
{
w += aw[i][k];
val += av[i][k];
}
if (j >= w)
f[j] = max(f[j], f[j - w] + val);
}
}
}
cout << f[n] << endl;
return 0;
}练习
说明:有依赖的背包原生题池很窄,几乎以 P1064(主件-附件两层)与 P2014(依赖成树)为双核。下面给两层依赖的 P1064 夯实归约;更一般的树上依赖(如 P2014 选课)在 F 部分树上背包展开,不在此重复。

