基环树入门
定义
基环树(Pseudo-tree),有时也被称为环套树,是图论中的一个概念。严格来说,它并不是一棵树,而是一个包含 n 个节点和 n 条边的连通图。
从结构上看,一棵 n 个节点的树有 n-1 条边,如果在树上任意两个节点之间添加一条边,就会形成一个且仅有一个环。因此,基环树可以被理解为一棵树加上一条额外的边所构成的图。
如果图不保证连通,那么一个含有 n 个节点和 n 条边的图可能是一个基环树森林(Pseudo-forest),即由若干个基环树组成。
无向基环树
在无向图中,基环树的结构就是一个简单的环,环上的每个节点都可能连接着一棵或多棵子树。

有向基环树
在有向图中,根据边的方向,基环树可以分为两种主要类型:
内向基环树 (In-tree)
每个节点都恰好有一条出边(出度为 1)。其形态表现为,所有节点最终都通过有向边汇聚到一个环上。环外的节点构成了若干棵树,树边全部指向环的方向。

外向基环树 (Out-tree)
每个节点都恰好有一条入边(入度为 1)。其形态与内向树相反,所有边都从环开始,向外发散。环外的节点也构成若干棵树,树边全部背离环的方向。

通用处理思路
处理基环树相关的问题,通常有一个经典的分析框架:
- 找到环:首先,通过算法定位图中唯一的环。
- 处理子树:将环上的每个节点视为根,其向外连接的部分就构成了一棵棵独立的子树。我们可以利用树形算法(如树形 DP)对这些子树进行预处理。
- 环上计算:将子树的处理结果整合到环上,再利用适用于序列或环的算法(如线性 DP、单调队列优化等)来解决整个问题。
如何找环
在基环树中找到环是解决问题的第一步。最常用的方法是深度优先搜索 (DFS)。
算法思路
- 从任意一个未访问过的节点开始进行深度优先搜索。
- 在搜索过程中,我们需要记录每个节点的父节点(即从哪个节点访问到当前节点的),同时用一个
visited数组记录节点是否被访问过。 - 当从节点
u访问邻接节点v时:- 如果
v未被访问,则继续向v深入搜索,并记parent[v] = u。 - 如果
v已被访问过,并且v不是u的父节点,那么就意味着我们找到了一个环。节点v是环的入口,而(u, v)是一条“返祖边”。
- 如果
- 找到环后,我们可以从
u开始,通过parent指针不断回溯,直到再次遇到v。这条回溯路径上的所有节点,再加上v,就构成了基环树中的环。
这个过程可以通过在 DFS 中维护一个递归栈来实现,当遇到一个已在当前递归栈中的节点时,就找到了环。
相关问题
基环树模型在算法竞赛中很常见,典型的例题包括:
- 求基环树的直径
- 基环树上的动态规划问题
- 寻找图中距离某点最远的点
topsrt找环
topsort
思路 : 不停的删除度为1 的点,最会剩余的点就在环上.
dfs找环
思路:
容易想到
- 记录每个的点的dfs序
dfn[u] - 在树上dfs时,不可能遇到
返组边,已经访问过的更大dfn值的点 - 在基环树上
返祖边,已经访问过的更大dfn值的点各一次,且是同一条边
于是我们可以写出下面的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// loop: 存储环路上的节点,loop_cnt: 环路上的节点数量
int loop[maxn],loop_cnt;
// dfn: 节点在DFS过程中的访问时间戳 (discovery time)
// dfn_idx: 时间戳计数器
int dfn[maxn],dfn_idx;
int fa[maxn]; // fa: 在DFS树中,节点的父节点
/**
* @brief 通过DFS寻找图中的环.
* 该函数适用于有向图,它寻找由前向边构成的环.
* @param u 当前访问的节点
* @param father u在DFS树中的父节点
*/
void get_ring(int u, int father)
{
// 为当前节点u分配一个访问时间戳
dfn[u] = ++dfn_idx;
// 记录u的父节点
fa[u] = father;
for (int i = e.h[u]; i != -1; i = e[i].next)
{
int v = e[i].v; // 邻接点
// 如果v已经被访问过
if (dfn[v])
{
// 如果v是u的祖先节点(返祖边),则跳过
if (dfn[v] < dfn[u]) continue; // 返祖边略过
// 找到一个环: u -> v, 并且v是u的后代(前向边)
// v的时间戳比u大,说明v在u的DFS子树中
// 从v开始,通过父节点指针回溯到u,记录路径上的所有节点
for( ; v != u ; v = fa[v]) loop[++loop_cnt] = v;
loop[++loop_cnt] = u;
}
else get_ring(v, u);
}
}
点击
好的,我们来详细拆解和理解这行代码 if (dfn[v] < dfn[u]) continue; 的作用。
这行代码是利用深度优先搜索(DFS)在基环树中找环的关键部分。为了理解它,我们需要先明白dfn数组和整个DFS过程的含义。
1. 基础概念
- 基环树 (Unicyclic Graph): 一个有 N 个点、N 条边的连通图。它的结构可以看作是一棵树,上面额外多了一条边,从而形成了一个唯一的环。
- DFS (深度优先搜索): 代码中的
get_loop函数本质上是一个DFS。 dfn[u](Discovery Time / Timestamp):dfn是 “depth-first number” 的缩写。dfn[u] = ++idx;这行代码给每个节点u在DFS中被访问到的顺序打上了一个时间戳。先被访问到的节点dfn值小,后被访问到的dfn值大。fa[u](Parent): 记录在DFS树中,节点u是由哪个节点访问过来的。fa[u]是u的父节点。- 找环的原理: 在DFS过程中,如果从当前节点
u访问到一个邻居节点v,而v之前已经被访问过了(即dfn[v]不为0),并且v不是u的父节点(v != fa[u]),那么我们就找到了一条“返祖边”,说明发现了一个环。
2. 核心问题:为什么要有 if (dfn[v] < dfn[u]) continue;?
在基环树的DFS中,当找到环时,实际上会有两次机会检测到这个环。
让我们用一个例子来说明。假设环是 A - B - C - D - A。我们的DFS从环外的一个点开始,通过 E 进入了环,访问顺序是 E -> A -> B -> C -> D。
此时的dfn和fa状态如下:
dfn[E] < dfn[A] < dfn[B] < dfn[C] < dfn[D]fa[A] = E,fa[B] = A,fa[C] = B,fa[D] = C
现在,DFS到达了节点 D。
第一次相遇 (深节点 D 遇到浅节点 A)
- 当前节点:
u = D。 - 遍历邻居:
D的邻居有C和A。- 当邻居是
C时,C == fa[D],所以continue,这是为了防止DFS走回头路。 - 当邻居是
A时,v = A。
- 当邻居是
- 判断:
v != fa[u](A != C) -> Truedfn[v](dfn[A]) > 0 -> True,说明A被访问过,我们找到了环!
- 进入关键代码:
if (dfn[v] < dfn[u])dfn[A] < dfn[D]是成立的,因为A比D先被访问。- 所以
continue被执行。 - 这次相遇被忽略了!
为什么忽略?因为在这个时刻,A 是 D 在DFS树中的祖先。如果我们在这里处理环,逻辑会比较复杂。而这段代码的设计者选择了一种更简洁的处理方式。
第二次相遇 (浅节点 A 遇到深节点 D)
- DFS从
D返回,然后从C返回,从B返回,最终回到A。 - 当前节点:
u = A。A已经完成了对邻居B的所有深度搜索。 - 遍历邻居:
A的邻居有E,B,D。- 当邻居是
E时,E == fa[A],continue。 - 当邻居是
B时,对B的DFS已经完成,B是A的子节点。 - 当邻居是
D时,v = D。
- 当邻居是
- 判断:
v != fa[u](D != E) -> Truedfn[v](dfn[D]) > 0 -> True,D被访问过,我们又找到了环!
- 进入关键代码:
if (dfn[v] < dfn[u])dfn[D] < dfn[A]不成立,因为dfn[D] > dfn[A]。- 所以
continue不被执行。
- 执行找环逻辑:
loop[++cnt] = v;//D被加入环for (; v != u; v = fa[v])// 从D开始,通过fa指针往回走v=D,fa[D]=C。C被加入环。v=C,fa[C]=B。B被加入环。v=B,fa[B]=A。A被加入环。v变成A,循环条件v != u(A != A) 为假,循环结束。
此时,环上的所有节点 (D, C, B, A) 都被正确地找到了。
总结
这行代码 if (dfn[v] < dfn[u]) continue; 是一个巧妙的过滤器。
它的核心思想是:只在环上dfn值最小的那个节点(也就是DFS最先进入环的那个节点)去处理和记录整个环。
dfn[v] < dfn[u]的情况:- 这表示当前节点
u找到了一个它的祖先v,形成了一个环。 - 我们选择忽略这次发现,让DFS继续回溯。
- 这表示当前节点
dfn[v] > dfn[u]的情况:- 这表示当前节点
u(一个祖先) 找到了一个它的后代v,而这个后代v之前已经被访问过了(说明u和v在同一个环上,并且v是通过另一条路径被访问的)。 u就是这个环上dfn值最小的节点。- 这时,我们才开始执行找环的逻辑。因为从
v到u的路径已经通过fa数组完整地记录下来了,可以方便地回溯并记录所有环上的节点。
- 这表示当前节点
通过这种方式,可以保证环只被发现和处理一次,并且是在一个最方便处理的位置(环的“顶部”),使得代码逻辑变得非常清晰和简单。
代码
dfs
思路
代码