[tarjan强连通分量算法] 目的,原理,思路,图解,伪代码,实例
强连通分量算法(Tarjan's Strongly Connected Component Algorithm)
利用深度优先算法找到一个非强连通的有向图中的所有强连通子图。无向图可以被认为是同时具备u->v和v->u的图。
一些概念
-
强连通:在有向图中,任意点u与v之间存在有来回两个方向的通路,类似存在一个环;
-
强连通图:图中任意两点存在强连通;
-
强连通分量:图中存在强连通的子图;
-
DFN(u):定义为第一次访问点u时的时间戳;
-
LOW(u):定义为u或u的子树能够回溯到的最早的栈中节点的DFN(u)。
原理 + 思路
当dfn[u] = low[u]时,以u为根的子树上所有节点是一个强连通分量。
-
首次访问某点u时,令low[u] = dfn[u] = index(or time)(初始化)
-
每到达一个点u,将其入栈(dfs的内容)
-
遍历所有与点u相连的点v:
-
若v不在栈中(树枝边)即未被访问过,递归方法继续往v后面搜索(如dfs()/tarjan()),最终会得出新的low[v];即low[u]=min(low[u], low[v]);
-
若v在栈内即已被访问,通过比较low[u]和v第一次被访问时的序列号/次数,来更新low[u],即low[u] = min(low[u], dfn[v])。
-
-
通俗一点的解释,来自GFG:
假设点u现在需要前往一个老前辈家里,这必须是一个非常非常非常老的前辈;
dfn代表点u自己的编号(第一次被访问到时的序号),low则是点u通过自己的小孩能联系到的最老的前辈(其子树所能到达的最远的祖先节点)。但是初始时我们不知道点u是否有小孩,所以dfn = low,即点u就是它自己的祖先节点;然后我们带着u去找它所有的小孩(程序去遍历u的所有子节点),查看是否有小孩能把点u带到更远的祖先那里去。如果有,我们就更新点u的low值;如果一个都没有,那么点u就准备下车回家(退栈)。注意在这过程中所有的小孩也要遍历一遍自己的小孩,来更新自己的值,以此类推,知道实在没有小孩了为止(无后续子节点)。
当然该过程中有两种情况:
-
之前没问过小孩(visited = false):递归求得小孩的low值,更新点u的low值;
-
之前有人问过小孩了(visited = true):说明这个"小孩"其实比点u还大(index值靠前,先入栈的),那点u就更新自己的low值,把这个"小孩"当成祖先。
如果有复数个子节点可以把点u带到更远的祖先那里,那么程序就看哪一个祖先的首次序列号dfn更小(即更靠前),点u就去哪。
图解(来自史上最清晰的Tarjan算法详解-segementFault)
强连通分量:[{0,1,2,3},{4},{5}]
第一步:从节点0开始
-
首先访问 0 -> 2 -> 4 -> 5并将其加入栈。根据访问顺序标记为1至4。这里也可以从0往1走。
-
对于节点5(栈内第一个元素),没有后续的节点了,检查到dfn[5] = low[5] = 4,所以退栈,得{5}为一个强连通分量。
第二步:返回节点4
-
节点4只有一个子树(节点5,已退栈),因此low[4] = min(low[4], low[5]) = low[4] = 3。
-
节点4后无其他子节点,且low[4] = dfn[4],退栈,得到第二个强连通分量。
第三步:返回节点2,并访问节点3及其子树
-
low[2] = min(low[2], low[4]) = 2,节点4(已出栈)是其中一个子节点, 节点2仍有后续子节点;
-
继续探索节点2的另一个子节点3。节点3入栈,初始化dfn[3] = low[3] = 5;
-
发现节点3有通向节点0的路径,且节点0仍在栈中,有low[3] = min(low[3], dnf[0]) = 1;
-
发现节点3有通向节点5的路径,但5已退栈,无需更新low[3]。
第四步:从节点3返回节点2
-
根据刚刚得到的新low[3]更新low[2] = min(low[2], low[3]) = 1;
-
节点2无其他后续子节点,返回0
第五步:返回节点0,并访问节点1及其子树
-
根据low[2]更新low[0],low[0] = min(low[0], low[2]) = 1;
-
发现节点1,dfn[1] = low[1] = 6;
-
发现后续节点3且3还在栈内,low[1] = min(low[1], dfn[3]) = 5
第六步:返回节点0
- dfn[0] = low[0] = 1, 退栈至0,得
伪代码
void tarjan(int u) { ++index; dfn[u] = low[u] =index; // 节点u的初始值 stack.push(u); // 入栈 for (auto& e : edges[u]) { // 遍历与u相连的点 if (!visited[e]) { // 若e未被访问过 tarjan(e); // 继续向下找其子节点 low[u] = min(low[u], low[v]); } else if (e is in stack) { // 若e被访问过且还在栈内 low[u] = min(low[u], dfn[e]); } } if (dfn[u] == low[u]) { // 遍历完所有子节点后,检查是否相等 while (!stack.empty()) { // 循环退栈并打印 v = stack.pop(); print v; } } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~