图论树论常见总结
想提升图和树题的“思维广度”,最有效的不是背更多算法,而是建立一套判断流程:
先识别结构,再把题意翻译成标准模型,最后根据数据范围选择工具。
下面给你一套可以长期使用的框架。
一、拿到图论题,先问这 8 个问题
1. 图是什么类型?
先在草稿上标出来:
- 有向还是无向?
- 有权还是无权?
- 权值是否可能为负?
- 连通还是不连通?
- 是否保证无环?
- 是否有重边、自环?
- 图是静态的还是会不断加边、删边?
- 图比较稀疏还是稠密?
这些信息经常直接决定算法。
例如:
无向 + 连通 + n-1 条边
立刻得到:
这是一棵树
有向 + 无环
立刻想到:
拓扑排序、DAG DP
无权图最短路
立刻想到:
BFS
2. 题目到底在问什么?
常见目标可以归为:
- 连通性;
- 最短路;
- 最长路;
- 最小生成树;
- 路径查询;
- 环;
- 遍历所有边;
- 选择一些点或边;
- 最大值最小、最小值最大;
- 动态加边、删边;
- 路径数量、方案数量。
把题目中的故事去掉,翻译成一句数学语言。
例如之前的巡逻题:
建立新边,使闭合巡逻路线最短
可以翻译为:
基础代价 2(n-1),最大化新边带来的节省
这种“答案 = 固定基础值 - 最大收益”是很常见的转换。
3. 数据范围允许什么复杂度?
大致可以这样判断:
| 数据范围 | 常见复杂度 |
|---|---|
| (n\le 20) | 状压、指数搜索 |
| (n\le 500) | (O(n^3)) 可能可以 |
| (n\le 5000) | 考虑 (O(n^2)) |
| (n\le 2\times10^5) | 通常需要 (O(n)) 或 (O(n\log n)) |
| (n,m\le 10^6) | 尽量线性 |
先想一个暴力做法,然后看它慢在哪里。
不要一开始就强行猜高级算法。
二、一般图的常用模型
1. 连通性
看到:
能否互相到达
连通块数量
不断合并两个集合
考虑:
- 静态图:DFS/BFS;
- 只加边、不删边:并查集;
- 离线删边:倒序变成加边;
- 更复杂动态连通:可撤销并查集、线段树分治。
并查集擅长维护:
哪些点属于同一个集合
但它本身不知道集合内部的路径结构。想维护更多信息,需要在根节点额外保存,例如:
size[root]
diameter[root]
edgeCount[root]
2. 最短路
| 边权情况 | 常用算法 |
|---|---|
| 所有边权都是 1 | BFS |
| 边权只有 0 和 1 | 0-1 BFS |
| 边权非负 | Dijkstra |
| 有负边 | Bellman-Ford 或特殊结构 DP |
| 有向无环图 | 拓扑排序后 DP |
| 点数很小,求任意两点 | Floyd |
一个重要习惯:
看到“最短路”先看边权,不要直接写 Dijkstra。
3. 最小生成树
看到:
连接所有点
选择一些边
总权值最小
不要求原图中的路径最短
考虑:
- Kruskal:边排序 + 并查集;
- Prim:不断选择连接当前集合的最小边。
注意最短路和最小生成树不同:
最短路:关注一个点到另一个点的距离
生成树:关注连接所有点的总成本
4. 有向无环图
看到:
任务依赖
先做 A 才能做 B
课程顺序
无环有向图
考虑:
- 拓扑排序;
- 拓扑序上的 DP;
- 判断环;
- 最长链、最短链、方案数。
DAG 上的最长路是容易做的;一般图的最长简单路径通常很难。
5. 强连通、桥和割点
看到:
删掉一条边后不连通
删掉一个点后不连通
有向图中互相可达
考虑:
- 无向图桥、割点:Tarjan;
- 有向图强连通分量:Tarjan/Kosaraju;
- 缩点后得到 DAG,再进行 DP。
6. 二分图、匹配、网络流
看到:
人和任务配对
左边集合与右边集合匹配
每个对象最多选一次
考虑二分图匹配。
看到:
容量限制
资源从源点流向汇点
若干条不相交路径
分配问题
考虑最大流、最小费用最大流。
7. 欧拉路和哈密顿路要分清
欧拉路:
每条边经过一次
核心看节点度数。
哈密顿路:
每个点经过一次
一般图中通常很难,不要把两者混淆。
三、树为什么特殊?
树有几个极强的性质:
- 任意两点之间只有一条简单路径;
- 有
n-1条边; - 任意删掉一条边都会断开;
- 任意加入一条边都会形成唯一的环;
- 选定根以后,每个节点只有一个父亲;
- 子树之间除了父节点外互不相交。
很多树题都是利用“唯一路径”把全局问题拆成局部问题。
所以拿到树题后,可以依次想:
能否定根?
能否从儿子向父亲汇总信息?
一条路径能否拆成两条向下链?
能否从叶子反向处理?
能否把子树变成一个连续区间?
四、树上的基础工具
1. DFS/BFS 和定根
定根之后:
无向树
↓
父子关系
↓
子树结构
最基础的树形 DP 框架:
void dfs(int u, int father) {
// 初始化 u 的状态
for (int v : tree[u]) {
if (v == father) continue;
dfs(v, u);
// 用儿子 v 的状态更新 u
}
}
所有状态都必须先用一句话定义清楚,例如:
dp[u] 表示以 u 为根的子树中的答案
或者:
dp[u] 表示从 u 向下走的最大收益
状态含义含糊,转移几乎一定会写乱。
2. 树的直径、中心和半径
树的直径:
树上最长简单路径
无权树可以两次 DFS/BFS:
- 任意点出发找最远点
s; - 从
s出发找最远点t; s-t是直径。
树的半径:
[
\left\lceil\frac{\text{直径}}2\right\rceil
]
树的中心是直径中间的一个或两个点。
触发词:
树上最远距离
最长路径
选一个点使最远距离最小
但不要看到树就立刻求直径。分叉结构、选多个点等问题,往往不能只看直径。
3. 最长链与次长链
任何一条经过节点 u 的路径都可以拆成:
某个子树向上到 u + 从 u 向下到另一个子树
所以维护:
first[u]:从 u 向下的最长链
second[u]:从 u 向下的次长链
经过 u 的最大路径:
first[u] + second[u]
这适合:
- 求树的直径;
- 求最大带权路径;
- 从两个不同子树选择路径;
- 前面巡逻题中的第二条净收益路径。
重点是:
最长链和次长链必须来自不同儿子方向。
4. 换根 DP
普通树形 DP只知道“子树内部”。
如果每个点都要作为根计算答案,就考虑换根 DP:
- 第一次 DFS:计算子树贡献;
- 第二次 DFS:把父亲方向的贡献传给儿子。
触发词:
对每个节点求答案
每个点作为起点
所有点到其他点的距离和
删掉某个点后的最大部分
典型例子:
求每个点到所有点的距离之和
5. 树上背包
看到:
选择恰好 k 个节点
在每棵子树中选择若干个
选择节点且满足父子限制
考虑树形背包。
常见状态:
dp[u][j]
表示在 u 的子树中选择 j 个点的最优答案。
合并儿子时枚举:
当前已经选多少
从新儿子的子树中再选多少
复杂度经常是 (O(nk^2)) 或经过优化后的 (O(nk))。
6. LCA
看到大量两点路径问题:
两点距离
路径长度
路径上的公共祖先
考虑最近公共祖先 LCA。
无权树中:
[
dist(u,v)=depth[u]+depth[v]-2depth[LCA(u,v)]
]
一般使用倍增,预处理 (O(n\log n)),单次查询 (O(\log n))。
7. DFS 序
一次 DFS 可以让某个节点的整棵子树对应一个连续区间:
子树 u ↔ [dfn[u], out[u]]
于是:
子树修改、子树查询
可以转换为:
数组区间修改、区间查询
然后使用:
- 前缀和;
- 树状数组;
- 线段树。
8. 树链剖分
DFS 序只能直接处理子树。
如果题目是:
修改 u-v 路径
查询 u-v 路径
考虑树链剖分,把一条树上路径拆成 (O(\log n)) 个连续区间,再交给线段树。
9. 删叶子
看到:
树的中心
从外向内缩小
保留连通核心
最远距离最小
考虑一层一层删叶子。
删一层叶子,相当于从树的边界向内推进一个单位。
刚才的核心城市题就是:
删到剩余点数不超过 k
删了多少层就是最小最大距离。
10. 点分治
看到大量距离相关问题,并且普通 DFS 很难:
树上是否存在距离为 k 的点对
统计距离不超过 k 的点对
动态查询某点附近的信息
考虑点分治。
这是偏高级的内容,可以等 LCA、树形 DP、树链剖分熟练后再学。
五、最重要的题意转换技巧
1. 最大值最小:固定答案检查
看到:
最小化最大距离
最大化最小距离
先考虑:
如果答案固定为
R,能否判断是否可行?
若存在单调性:
R 可行,则更大的 R 也可行
就可以二分答案。
典型题:
- 最小化最大分段和;
- 最大化点之间最小距离;
- 最小化覆盖半径;
- 限定最长路径后能否完成。
不是所有题都需要二分。有时固定答案后可以直接逐层推进,比如删叶子。
2. 正着选择困难,就反着删除
例如:
选择哪些点作为核心
正面选择有很多组合。
反过来想:
哪些边缘点可以安全删除?
往往立刻变成删叶子。
其他常见反向思考:
动态删边 → 倒序加边
最后状态 → 从最后一步向前推
选择保留什么 → 考虑删除什么
3. 基础代价加收益
先计算一个必然可行的基础答案:
基础答案
然后问每个操作能节省多少:
[
答案=基础代价-最大收益
]
巡逻题中:
基础代价 = 2(n-1)
新路对应路径的边可以少走一次
因此问题变成最大化路径收益。
这种思路常出现在:
- 重复经过边;
- 修改少量边;
- 增加少量设施;
- 在已有方案上进行优化。
4. 按点贡献或按边贡献
如果直接计算整体很难,可以问:
每个点贡献多少?
每条边贡献多少?
树上一条边被多少条路径经过,通常可以通过切断这条边后的两侧大小计算。
例如一条边切开后两边大小为 s 和 n-s,经过这条边的点对数量是:
[
s(n-s)
]
这种“边贡献”非常常见。
5. 加一条边,先看唯一形成的环
树中加入一条 u-v 边,会形成唯一的环:
新边 + 原树中的 u-v 路径
因此所有“树上加一条边”的题,都应该先研究原来的 u-v 路径。
之前巡逻题就是典型例子。
6. 路径问题找最高点
定根后,任何路径 u-v 都可以拆成:
u → LCA(u,v)
LCA(u,v) → v
树形 DP 中,一条路径也可以看成:
从某个儿子向上到 u
再从 u 向下到另一个儿子
所以:
最长链 + 次长链
本质上是在枚举路径的最高点。
7. 只保存未来真正需要的信息
动态森林直径题中,一个连通块未来合并时只需要:
并查集根 + 当前直径
不需要真的加入新边、重建树。
这种思想叫“状态压缩”:
不保存整个历史,只保存足够决定未来的统计量。
做动态题时经常问:
未来操作究竟需要知道当前结构的哪些信息?
六、如何设计树形 DP
设计树形 DP时按这四步来。
第一步:定义状态
一定写成完整句子:
dp[u] 表示……
例如:
dp[u] 表示从 u 出发,只向子树方向走的最大路径权值
第二步:考虑叶子
叶子没有儿子,状态应该是什么?
例如最长向下链:
dp[leaf] = 0;
叶子的初始化经常能帮助你检验状态定义是否正确。
第三步:考虑一个儿子如何贡献
假设已经知道 dp[v],它怎样更新 dp[u]?
gain = dp[v] + weight(u,v);
第四步:答案在哪里产生?
答案可能:
- 就是根节点的
dp[root]; - 在每个节点合并两个儿子时产生;
- 需要第二次换根;
- 需要对所有
dp[u]取最大值。
最长路径问题的答案通常不是 dp[root],而是:
max(first[u] + second[u])
七、正确性证明的常用模板
1. 下界 + 构造
先证明任何方案都至少是 X,再构造一个恰好达到 X 的方案。
例如合并两棵树的直径:
任何连接方式至少受到原直径和半径的限制
连接两个中心可以达到这个下界
2. 交换论证
假设最优方案不是我们想要的结构,证明可以把其中一部分替换掉,并且答案不会变差。
常用于:
- 贪心;
- 最小生成树;
- 区间选择;
- 排序策略。
3. 子树归纳
先假设所有儿子的状态正确,再证明按照转移得到的父节点状态正确。
这是树形 DP 最标准的证明。
4. 唯一路径分解
证明树上一条路径一定经过某个节点或某条边,然后枚举这个关键位置。
最长、次长链就是在枚举路径的最高点。
5. 单调性证明
二分答案前必须证明:
R 可行 ⇒ R+1 一定可行
不能因为答案是数字就直接二分。
八、常见误区
1. 看到树就求直径
直径适合“单条最长路径”或“单个中心”。
但如果选择可以分叉,例如选多个连通节点,直径可能无法描述全部结构。
2. 把最长和次长直接相加
必须确认它们来自不同方向,否则可能重复经过同一条边。
3. 负权树仍然两次 DFS 求直径
两次找最远点的方法依赖边权非负。
出现负权后,使用树形 DP求最大路径更稳妥。
4. 节点数和边数混淆
路径:
v1-v2-...-vk
有 k 个节点,但只有 k-1 条边。
5. DFS 全局变量没有重置
两次 DFS 前常见的:
res = -1;
w = start;
必须重新初始化。
6. 递归爆栈
树可能是一条几十万节点的链。
逻辑正确但运行时错误时,要考虑:
- 改为迭代 DFS;
- 显式维护栈;
- 用正序和逆序模拟后序遍历。
7. 状态含义变化
不能一会儿让:
dp[u] = 子树直径
一会儿又让它表示:
dp[u] = 向下最长链
需要不同数组或清晰命名。
九、建议的学习顺序
可以按下面顺序练习。
第一阶段:图的遍历
- DFS/BFS;
- 连通块;
- 二分图染色;
- 拓扑排序;
- 无权最短路。
第二阶段:树的基础
- 子树大小;
- 深度和父亲;
- 树的直径;
- 树的中心;
- 最长链、次长链;
- 删叶子。
第三阶段:树形 DP
- 最大独立集;
- 树上选择与不选择;
- 换根 DP;
- 树形背包;
- 带权最大路径。
第四阶段:路径查询
- LCA;
- 树上差分;
- DFS 序;
- 树状数组和线段树;
- 树链剖分。
第五阶段:高级图论
- 强连通分量;
- 桥和割点;
- 最小生成树;
- 网络流、二分图匹配;
- 点分治、可撤销并查集。
十、做题时可以使用的检查表
以后遇到图或树题,可以在草稿上快速写:
1. 图的类型是什么?
2. 数据范围允许什么复杂度?
3. 去掉故事后,目标是什么?
4. 先写出的暴力是什么?
5. 重复计算在哪里?
6. 是点贡献、边贡献,还是路径贡献?
7. 最大最小问题能否固定答案?
8. 正着选择困难,能否反向删除?
9. 树上能否定根并从儿子合并?
10. 最终答案在根、某个节点,还是某条路径上?
你最近做的三道题刚好代表三种非常典型的转换:
| 题目 | 表面问题 | 核心转换 |
|---|---|---|
| 动态森林直径 | 不断连接连通块 | 并查集维护连通块直径 |
| 巡逻加路 | 新增道路减少巡逻距离 | 基础代价减收益,重合边赋 -1 |
| 核心城市 | 选择连通的 k 个点 |
从边缘逐层删叶子 |
真正要积累的不是这三道题的代码,而是这三个信号:
动态合并 → 并查集
基础方案上优化 → 计算收益
连通核心、最远距离 → 从叶子反向缩小
当这些信号见得足够多,你思考新题时就不会只有“DFS 或直径”一条路,而会自然地同时尝试状态压缩、反向处理、贡献统计、树形 DP和固定答案检查。

浙公网安备 33010602011771号