dfn序优化树上背包

给出树上背包经典题型链接

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]

相关推荐
MicroTech20258 小时前
微算法科技(NASDAQ MLGO)区块链混合检测模型优化确保全网防御策略一致性
科技·算法·区块链
LYFlied8 小时前
【每日算法】 LeetCode 394. 字符串解码
前端·数据结构·算法·leetcode·面试·职场和发展
董世昌418 小时前
break和continue的区别是什么?
java·jvm·算法
夏鹏今天学习了吗8 小时前
【LeetCode热题100(75/100)】跳跃游戏 II
算法·leetcode·游戏
lxh01138 小时前
复原IP地址
前端·数据结构·算法
元亓亓亓8 小时前
LeetCode热题100--45. 跳跃游戏 II--中等
算法·leetcode·游戏
Christo38 小时前
NIPS-2022《Wasserstein K-means for clustering probability distributions》
人工智能·算法·机器学习·数据挖掘·kmeans
xiaolongmeiya8 小时前
P3810 【模板】三维偏序 / 陌上花开 cdq分治+树状数组
c++·算法
LYFlied8 小时前
【每日算法】LeetCode 20. 有效的括号
数据结构·算法·leetcode·面试