1 问题引入
真实场景里,物品之间常有「要买 A 必须先买 B」的约束:
有
样例:
7 10
2 3 0
3 4 1
4 5 1
1 2 0
2 6 4
3 3 4
1 1 0
读作:1 和 4、7 是主件;2、3 依赖 1;5、6 依赖 4。
2 先看简化版:主件 + 至多两个附件
这就是 NOIP2006「金明的预算方案」的模型。每个主件最多带两个附件,且附件不再有附件。
一个主件和它的附件,可能的买法只有几种:
- 都不买;
- 只买主件;
- 主件 + 附件 1;
- 主件 + 附件 2;
- 主件 + 两个附件。
这五种买法互斥(你只能选一种),所以「一个主件及其附件」恰好是一个物品组——正是分组背包的模型。把每种买法算成一个「物品」,组内至多选一个,用分组背包求解:
// 有依赖的背包(简化版,NOIP2006 金明的预算方案):每个主件最多 2 个附件
//
// 一个主件和它的附件,可能的买法是:不买、只买主件、主件+附件1、主件+附件2、
// 主件+两个附件 —— 至多 5 种,且互斥。所以「主件及其附件」等价于一个物品组,
// 用分组背包求解:容量倒序,组内枚举买法。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e3 + 5;
int n, C;
int f[maxn];
int w[maxn], v[maxn];
int att1[maxn], att2[maxn]; // 每个主件的第 1、2 个附件的编号,0 表示没有
int main() {
cin >> n >> C;
int mainIdx[maxn], mainCnt = 0;
for (int i = 1; i <= n; ++i) {
int p;
cin >> w[i] >> v[i] >> p;
if (p == 0) mainIdx[++mainCnt] = i;
else if (att1[p] == 0) att1[p] = i;
else att2[p] = i;
}
for (int t = 1; t <= mainCnt; ++t) {
int m = mainIdx[t];
int c1 = att1[m], c2 = att2[m];
// 先枚举容量(倒序),再枚举这一组的互斥买法
for (int j = C; j >= 0; --j) {
int best = f[j]; // 不买
if (j >= w[m]) // 只买主件
best = max(best, f[j - w[m]] + v[m]);
if (c1 && j >= w[m] + w[c1]) // 主件 + 附件1
best = max(best, f[j - w[m] - w[c1]] + v[m] + v[c1]);
if (c2 && j >= w[m] + w[c2]) // 主件 + 附件2
best = max(best, f[j - w[m] - w[c2]] + v[m] + v[c2]);
if (c1 && c2 && j >= w[m] + w[c1] + w[c2]) // 主件 + 两个附件
best = max(best, f[j - w[m] - w[c1] - w[c2]] + v[m] + v[c1] + v[c2]);
f[j] = best;
}
}
cout << f[C] << endl;
return 0;
}
样例输出:
16
main_attach.cpp 假设每个主件最多两个附件、且附件没有自己的附件。如果数据不满足这个前提(比如附件还带附件),它会给出错误答案。放宽这个限制要用下面的树形 DP。
3 一般情形:依赖是一棵森林
去掉「最多两个附件」的限制后,依赖关系就是一片森林:主件是根,附件是它的儿子,附件还能有附件。
加一个虚拟根
设
对每个点
- 先把
自己放进去: 时 ,否则无解; - 再依次把每个儿子
的 DP 数组合并进来。合并时枚举「分给已合并部分多少容量」:
- 合并完所有儿子后,
就是 这棵子树的答案。
由于
// 有依赖的背包:依赖关系是一棵森林,加虚拟根 0 变成一棵树
// 选了儿子就必须选父亲,所以对每个点 u:
// 1. 先把所有儿子的 dp 数组合并进来(每个儿子是一个「物品组」,至多选一个方案)
// 2. 再强制把 u 自己放进去(j >= w[u] 时才有值,否则无解)
// 儿子用链式前向星存,不依赖 STL 容器
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e3 + 5;
const int NEG = -1e9;
int n, C;
int w[maxn], v[maxn];
int f[maxn][maxn]; // f[u][j]:以 u 为根的子树,装了 u,花费 j 的最大价值
int head[maxn], nxt[maxn], to[maxn], ecnt; // 链式前向星存儿子
void add(int p, int u) { // p 是父亲,u 是儿子
to[++ecnt] = u; nxt[ecnt] = head[p]; head[p] = ecnt;
}
void dfs(int u) {
// 先把 u 自己放进去:花费 w[u] 才有价值 v[u]
for (int j = 0; j <= C; ++j)
f[u][j] = (j >= w[u]) ? v[u] : NEG;
for (int e = head[u]; e; e = nxt[e]) {
int son = to[e];
dfs(son);
// 合并儿子:枚举分给「已合并部分」多少容量
for (int j = C; j >= 0; --j) {
for (int k = 0; k <= j; ++k) {
if (f[u][j - k] == NEG || f[son][k] == NEG) continue;
f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);
}
}
}
}
int main() {
cin >> n >> C;
for (int i = 1; i <= n; ++i) {
int p;
cin >> w[i] >> v[i] >> p;
add(p, i); // p=0 时自然挂到虚拟根 0 下
}
dfs(0);
// 根 0 不占容量、不产生价值,答案就是 f[0][C](可以什么都不选)
cout << max(0, f[0][C]) << endl;
return 0;
}
样例输出:
17
合并儿子时,
4 和「泛化物品」的联系
把
这正是下一讲要讲的「泛化物品」。依赖关系树的每一棵子树都等价于一件泛化物品,求某个点对应的泛化物品,就等于把所有儿子的泛化物品加上自己,再求和。
所以树形 DP 的合并操作,本质上就是「泛化物品之和」的运算:
5 小结
有依赖的背包有两个层次:
- 浅层:主件 + 少量附件的固定组合 → 枚举所有互斥买法 → 变成物品组 → 分组背包。
- 深层:任意深度的依赖森林 → 加虚拟根 → 树形 DP,儿子合并进父亲,强制选父亲。
两种做法的共同点是把互斥的选择打包成一个「物品」。区别只是打包的方式:简化版手工枚举,一般版用 DP 算出来。
6 相关题解
暂无相关题解