给出树上背包经典题型链接
https://www.luogu.com.cn/problem/P1273
https://www.luogu.com.cn/problem/P2014
dfn序真的很重要,这道题学到很多,既了解了dfn序这个重要内容,又学习了用其优化树上背包,另外还有一个小细节,以后最大值这样定义#define INF 0x3f3f3f3f 不要再用INT_MAX,可能计算溢出
下面贴出第二道题目的解法(dfn序加巧妙思路优化树上背包)
#include <bits/stdc++.h>
using namespace std;
#define MAXN 3005
#define MAXM 3005
#define INF 0x3f3f3f3f
// ===== 建图(链式前向星)=====
int head[MAXN], nxt[MAXM], to[MAXM];
int cnt;
// ===== 题目数据 =====
int n, m;
int nums[MAXN]; // 原节点净收益(用户钱 - 边成本)
// ===== DFS + dfn =====
int dfncnt;
int value[MAXN]; // dfn 对应的净收益
int sizes[MAXN]; // dfn 子树大小
int id[MAXN]; // dfn -> 原节点编号
// ===== DP =====
int dp[MAXN][MAXM]; // dp[i][j]: dfn[i..] 选 j 个用户的最大净收益
void add(int u, int v) {
nxt[cnt] = head[u];
to[cnt] = v;
head[u] = cnt++;
}
void build() {
cin >> n >> m;
cnt = 1;
dfncnt = 0;
for (int i = 0; i <= n; i++) {
head[i] = 0;
nums[i] = 0;
}
for (int i = 1; i <= n - m; i++) {
int k;
cin >> k;
while (k--) {
int v, w;
cin >> v >> w;
nums[v] -= w; // 边费用转移到子节点
add(i, v);
}
}
for (int i = n - m + 1; i <= n; i++) {
int w;
cin >> w;
nums[i] += w; // 用户付费
}
}
int dfs(int u) {
int i = ++dfncnt;
id[i] = u; // 记录映射
value[i] = nums[u];
sizes[i] = 1;
for (int e = head[u]; e; e = nxt[e]) {
sizes[i] += dfs(to[e]);
}
return sizes[i];
}
int main() {
build();
dfs(1);
// ===== DP 初始化 =====
for (int i = 1; i <= n + 1; i++)
for (int j = 0; j <= m; j++)
dp[i][j] = -INF;
dp[n + 1][0] = 0;
// ===== 核心 DP =====
for (int i = n; i >= 1; i--) {
for (int j = 0; j <= m; j++) {
// 不选 i:整棵子树跳过
dp[i][j] = dp[i + sizes[i]][j];
// 选 i
bool isUser = (id[i] >= n - m + 1);
int need = isUser ? 1 : 0;
if (j >= need && dp[i + 1][j - need] != -INF) {
dp[i][j] = max(
dp[i][j],
value[i] + dp[i + 1][j - need]
);
}
}
}
// ===== 取答案 =====
for (int j = m; j >= 0; j--) {
if (dp[1][j] >= 0) {
cout << j << endl;
break;
}
}
return 0;
}
下面我把它叫做:
「dfn + 跳子树」树上背包模型
一、这套写法一句话定义
把树用 DFS 编号压成一条序列,
用"跳过整棵子树"来保证结构合法,
再在序列上做类似 01 背包的 DP。
二、它什么时候"能用"(✔ 必须满足)
只要 同时满足下面 4 条,你就要第一时间想到这套写法。
✅ 条件 1:图是「树 / 森林」
-
没有环
-
每个节点只有 一个父亲
-
或者可以 加一个虚拟根 变成树
📌 典型表述:
-
"转播网是树状结构"
-
"每个课程最多一个先修课"
-
"公司组织结构"
-
"依赖关系构成树"
✅ 条件 2:子节点依赖父节点
也就是:
❌ 不能选子节点而不选父节点
✅ 选了子节点,父节点必须被选
📌 题面常见说法:
-
"必须先完成 A 才能做 B"
-
"信号要经过中转站才能到用户"
-
"技能树"
-
"只有买了主件才能买配件"
这是这套 DP 能成立的结构约束来源。
✅ 条件 3:"选 or 不选"是核心决策
每个节点都是:
-
要么选
-
要么不选
而不是:
-
选多次
-
或连续选区间
📌 典型:
-
选课程
-
选用户
-
选任务
-
选节点
👉 本质是 0/1 决策
✅ 条件 4:目标是"数量 / 价值"的背包型目标
比如:
-
固定选 k 个,最大价值
-
价值 ≥ 0,数量尽量多
-
总成本 ≤ B,选的节点最多
📌 换句话说:
你能写出一个类似
dp[i][j]的定义
三、它什么时候"特别适合"(🔥 强烈推荐)
当你发现下面任意一条,优先用它,而不是传统树形背包。
🔥 情况 1:n 很大,m 也不小
-
n ≈ 2000~3000
-
m ≈ 2000~3000
❌ 传统树形背包:
O(n * m^2) → 直接 TLE
✅ dfn 跳子树:
O(n * m) → 稳稳 AC
👉 这是这套写法最大的价值
🔥 情况 2:树的"顺序"无所谓,只在乎结构合法
也就是说:
-
不关心兄弟节点谁先谁后
-
只要求:
-
父在子前
-
子必须连续
-
👉 正是 DFS 序的强项。
🔥 情况 3:叶子 / 某类节点才是"计数目标"
比如:
-
只数用户节点
-
只数课程
-
中转节点只是"通路"
📌 你这道转播题就是典型。
四、它什么时候「不适合」(⚠️ 千万别硬套)
下面这些情况,用它一定会把自己绕死。
❌ 1️⃣ 一个节点可以有多个父亲(DAG)
-
不是树
-
比如一般的 DAG 依赖
👉 这套方法 必须保证子树是连续区间
❌ 2️⃣ 选子节点不一定要选父节点
例如:
-
"买子配件可以不买主件"
-
"跳级选课"
👉 结构约束没了,i+size[i] 的跳跃就不成立。
❌ 3️⃣ 状态依赖"兄弟之间的组合方式"
例如:
-
"同一父节点下,最多选 2 个孩子"
-
"孩子之间互斥 / 协同"
👉 那必须回到 传统树形背包合并子树
❌ 4️⃣ 问题的核心不是"选节点"
比如:
-
路径问题
-
最近公共祖先
-
子树查询
👉 完全是另一类题。
五、如何一眼识别"这是这套模型的题"
你看到题目时,可以在脑子里快速过这 4 个问题:
1️⃣ 结构是不是树?
2️⃣ 子节点是不是依赖父节点?
3️⃣ 是不是 0/1 选择节点?
4️⃣ 能不能定义 dp[i][j]?