这杯零食给“会 DFS、但一看到
low就想把电脑合上”的同学。先把图压成更好理解的形状,再进正式课学严谨的一次 DFS 写法。
学完后,你能:
dfn/low/栈 各在避免什么重复工作。前置:有向边、无向边、DFS/BFS、连通块。请先回答:
这节有两张不同的图,千万别混在一张纸上:一张是有向图,问“能去又能回来”;另一张是无向图,问“删掉谁后断开”。每张纸都画四栏:我从谁出发、这一步沿了哪条边、我还能回到谁、这条边/点被删后谁失去联系。
遇到 low、缩点、桥这些词时,先把它们盖住。你要先亲自回答:哪些点真的扮演同一种“可达角色”?哪条路只是绕远,哪条路是唯一通道?
画有向边:A→B, B→C, C→A, C→D, D→E, E→D, E→F。从每个点出发,先只做最普通的 DFS/BFS,并把下表填成“能/不能”:
| 出发点 | 能到 A | 能到 C | 能到 D | 能到 F |
|---|---|---|---|---|
| A | ||||
| D | ||||
| F |
停顿 0:
A能到D,D能到A吗?哪些点既能从 A 到达、又能回到 A?先圈成一团。
若对每个点都做一次 DFS,就能得到完整的可达表,复杂度约为 $O(V(V+E))$。请在刚才的图上标色:从 A 的 DFS 会反复看 A,B,C 的环;从 B 再出发时,又会把同一个环里的边检查一遍。
停顿 1:把
A,B,C压成一个大圆,D,E压成一个大圆,F保留单点。压完后边还剩怎样的方向?有没有环?
请为下面三句各写一条路线拼接理由,而不是只写“显然”:
先接受一个适合手算的框架:第一次 DFS 不急着分组,只记录“一个点的所有后继都处理完”的完成时刻;第二次把所有箭头倒过来,按完成时刻从晚到早出发收集点。
请在下表中模拟从 A 的第一次 DFS。每当一个点没有未访问后继时,把它写进 finish:
| 返回到的点 | 此时哪些后继已处理完 | finish 尾部 |
|---|---|---|
| F | ||
| E | ||
| D | ||
| C |
停顿 3:为什么第二轮要把箭头反过来?若仍在原图按倒序出发,从 A 会不会一路跑到 D、E、F?
擦掉箭头,画边:A-B, B-C, C-A, C-D, D-E, E-F, F-D。先不用任何公式,做两个物理实验:
A-B;A 和 B 还能通过谁相遇?C-D;左右两坨还能相遇吗?停顿 4:给“删边断开”和“删点断开”各起一个临时名字,再看下方正式名字。
如果要知道任意两点是否互相可达,最暴力的做法是从每个点都 DFS 一遍。$n$ 个点会得到 $O(n(n+m))$ 的工作量;更糟的是,我们反复发现同一个环里的点本来就是“同一团”。
例如 $A\to B\to C\to A$,再有 $C\to D$,$D\leftrightarrow E$,$E\to F$。在团内从谁出发都能绕回来;真正有价值的是团与团之间的单向关系。
把 $u$ 与 $v$ 放同组,当且仅当 $u$ 能到 $v$ 且 $v$ 能到 $u$。它有三种好性质:自己能到自己、关系可以倒过来、两段来回路可以接起来。因此它是等价关系,所有点被自然切成若干个 SCC。
若缩点图还有一个环,例如团 $P\to Q\to R\to P$,那么三个团里的任何点都能沿这些箭头互相到达,本应是同一团,矛盾。所以缩点后的图是 DAG。这个结论让很多“乱图”变成可以按方向依次处理的图。
一种适合入门理解的办法是 Kosaraju:
为什么不会串到别团?把一团看成缩点 DAG 的一个点。第一次 DFS 中,沿箭头能到达的团会更早完成;倒序挑出的团不会在反图中走进尚未收集的“错误团”。正式课的 Tarjan 把这两次旅行压缩进一趟 DFS:low 负责告诉我们能回到多早,栈负责暂存尚未封口的一团。
finish = []
for every vertex u not visited:
dfs_original(u) and append u when it finishes
for u in reverse(finish):
if u not assigned:
component = dfs_on_reversed_graph(u)
output component
现在把箭头擦掉。若删一条边后图被切开,它是桥;若删一个点(以及相连边)后图被切开,它是割点。它们都在问:某棵 DFS 子树能否借一条边绕回祖先?
这里有一个容易混的对照:SCC 的 low 关注“仍在当前栈里的可回到位置”;桥/割点的 low 关注“子树能否绕到祖先”。名字相同,工作许可证不同,别把两张表硬嫁接。
先对有向图完成 Kosaraju 的第二轮。把第一轮的 finish=[F,E,D,C,B,A] 写在纸上,倒过来依次尝试 A,B,C,D,E,F。每次从尚未分配的点出发,在反图中记录本轮收进的点:
| 尝试起点 | 是否已分配 | 反图 DFS 收到的点 | 新分量 |
|---|---|---|---|
| A | |||
| B | |||
| C | |||
| D | |||
| F |
再对无向图做一个低链前的人工验证。删除 C-D 后,以 A 为起点能访问到哪些点?删除 A-B 呢?请把结果写进:
| 删除对象 | A 所在连通块 | 另一侧还有谁 | 结论 |
|---|---|---|---|
边 C-D |
|||
边 A-B |
|||
点 C |
在无向 DFS 树中,若孩子 v 的整棵子树不能沿返祖边回到 u 之前的祖先,即 low[v] > dfn[u],树边 $(u,v)$ 是桥。若非根 u 存在孩子使 low[v] >= dfn[u],删除 u 后那棵子树无法接到上方,u 是割点。根要特殊处理:它有至少两个 DFS 孩子时才是割点。
这些条件不是魔法公式:把 low[v] 读成人话——“从 $v$ 子树出发,最早还能绕回 DFS 树的哪位祖先”。
小图上暴力删边/删点当然可以用来验证答案,但每次重做连通性会是 $O((n+m)^2)$ 量级。dfn/low 让一趟 DFS 汇总所有绕路信息,达到 $O(n+m)$。
运行 从互相绕路到缩点.py。选择:
1:观察原图完成顺序、反图 DFS 和缩点结果;2:观察删桥/割点的低链值,并比较“有环可绕”和“桥无处绕”。先在纸上预测 $C\to D$ 所在的位置会不会把两个 SCC 合并,再让程序揭晓。程序不是神谕,它只是一个不嫌你反复按回车的黑板。
缩点 DAG 是许多 DP、最少补边和可达性题的起点;桥缩成边双连通分量后得到桥树;割点和点双连通分量会在后续圆方树中重现。现在只需牢牢记住两句话:有向图先看“能否回来”,无向图先问“有没有绕路”。
dfn/low 的人话含义。