资料参考
- Tarjan算法寻找有向图的强连通分量
- 基于强联通的tarjan算法详解
- 有向图强连通分量的Tarjan算法
- 处理SCC(强连通分量问题)的Tarjan算法
- 强连通分量的三种算法分析
- Tarjan算法详解理解集合
- ppt图解分析下载
强连通分量
强连通分量(strongly connected component)是图论中的概念。图论中,强连通图指每一个顶点皆可以经由该图上的边抵达其他的每一个点的有向图。意即对于此图上每一个点对(Va,Vb),皆存在路径Va→Vb以及Vb→Va。强连通分量则是指一张有向图G的极大强连通子图G‘。如果将每一个强连通分量缩成一个点,则原图G将会变成一张有向无环图。一张图被称为有向无环图当且仅当此图不具有点集合数量大于一的强连通分量,因为有向环即是一个强连通分量,而且任何的强连通分量皆具有至少一个有向环。
比如说这个有向图中,点 1,2,4,5,6,7,8 和相应边组成的子图就是一个强连通分量,另外点3,9 单独构成强连通分量
Tarjan 算法是由 Robert Tarjan 提出的用于寻找有向图的强连通分量的算法。它可以在O(|V|+|E|)的时间内得出结果。(运行Tarjan算法的过程中可以知道,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为O(|V|+|E|))
搜索树
Tarjan 算法主要是利用 DFS 来寻找强连通分量的。在介绍该算法之前,我们先来介绍一下搜索树。先前那个有向图的搜索树是这样的:
有向图的搜索树主要有 4 种边(这张图只有 3 种),其中用实线画出来的是树边(tree edge),每次搜索找到一个还没有访问过的结点的时候就形成了一条树边。用长虚线画出来的是反祖边(back edge),也被叫做回边。用短虚线画出来的是横叉边(cross edge),它主要是在搜索的时候遇到了一个已经访问过的结点,但是这个结点并不是当前节点的祖先时形成的。除此之外,像从结点1到结点6这样的边叫做前向边(forward edge),它是在搜索的时候遇到子树中的结点的时候形成的,容易知道移除前向边不会改变图的连通性,所以在讨论中基本忽略它。另外,还有从一棵搜索树上的节点到另一棵搜索树上的节点的边,称为跨树边,但是跨树边在这里也发挥不了大的作用。
Tarjan算法
Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。
Tarjan 算法主要是在 DFS 的过程中维护了一些信息:dfn、low 和一个栈。
- 栈里的元素表示的是当前已经访问过但是没有被归类到任一强连通分量的结点。
- dfn[u] 表示结点 u 在 DFS 中第一次搜索到的次序,通常被叫做时间戳。
- Low(u)为u或u的子树能够追溯到的最早的栈中节点的次序号(即最小的dfn值)(实际操作中low[i]不一定最小,但不会影响程序的最终结果)(另外一种关于low[u]的表述:记录该点所在的强连通子图所在搜索子树的根节点的Dfn值)
- 结点 u 是某个强连通分量的根等价于 dfn[u] 和 low[u] 相等。简单可以理解成当它们相等的时候就不可能从 u 通过子树再经过其它时间戳比它小的结点回到 u。(dfn[u] == low[u], 则u是分量子树的树根。 这是一个充要条件。 -->: 如果u 是树根,且由于是强分量,其子孙都可达到u, 则子孙的low 都是u 的编号。则low[u] = dfn[u]. <--: 如果low[u] == dfn[u], 假设u不是树根,那么肯定有u的祖先是树根,那么low[u]肯定比当前值小,矛盾,因此u是树根。)
算法伪代码
tarjan(u) { DFN[u]=Low[u]=++Index // 为节点u设定次序编号和Low初值 Stack.push(u) // 将节点u压入栈中 for each (u, v) in E // 枚举每一条边 if (v is not visted) // 如果节点v未被访问过 tarjan(v) // 继续向下找 Low[u] = min(Low[u], Low[v]) else if (v in S) // 如果节点v还在栈内 Low[u] = min(Low[u], DFN[v]) if (DFN[u] == Low[u]) // 如果节点u是强连通分量的根 repeat v = S.pop // 将v退栈,为该强连通分量中一个顶点 print v until (u== v) }
该算法有三个得注意的地方:
- Low[u] = min(Low[u], DFN[v])当搜索到一条u->v的边,若v仍在栈中,则v必为u的祖先,u->v为一条后向边,由于形成后向边,则必构成环,该环中的所有点必为一个强联通分量或强连通分量中的某些点,对于后向边,我们需要对Low进行更新,因为Low记录的是能够追溯到的最早的栈中节点的次序号,所以 Low[u] = min(Low[u], DFN[v])。
- 对于横跨边u->v,是不需要对Low[u]进行更新的若一个节点已经访问过,则它的DFN必有一个值,而当v已经被访问过,但已经不在栈中(已经是一个强连通分量中的点),则u,v不在同一DFST上,此时构成了横跨边,没有祖先孙子关系,不需要对Low值进行更新。
- 回溯的时候,对树枝边的Low[u]就行更新,Low[u] = min(Low[u], Low[v])
Tarjan算法的操作原理如下:
- Tarjan算法基于定理:在任何深度优先搜索中,同一强连通分量内的所有顶点均在同一棵深度优先搜索树中。也就是说,强连通分量一定是有向图的某个深搜树子树。
- 可以证明,当一个点既是强连通子图Ⅰ中的点,又是强连通子图Ⅱ中的点,则它是强连通子图Ⅰ∪Ⅱ中的点。
- 这样,我们用low值记录该点所在强连通子图对应的搜索子树的根节点的Dfn值。注意,该子树中的元素在栈中一定是相邻的,且根节点在栈中一定位于所有子树元素的最下方。
- 强连通分量是由若干个环组成的。所以,当有环形成时(也就是搜索的下一个点已在栈中),我们将这一条路径的low值统一,即这条路径上的点属于同一个强连通分量。
- 如果遍历完整个搜索树后某个点的dfn值等于low值,则它是该搜索子树的根。这时,它以上(包括它自己)一直到栈顶的所有元素组成一个强连通分量。
大致证明:
- 在栈里,当dfs遍历到v,而且已经遍历完v所能直接到达的顶点时,low[v]=dfn[v]时,v一定能到达栈里v上面的顶点:因为当dfs遍历到v,而且已经dfs递归调用完v所能直接到达的顶点时(假设上面没有low=dfn),这时如果发现low[v]=dfn[v],栈上面的顶点一定是刚才从顶点v递归调用时进栈的,所以v一定能够到达那些顶点。
- dfs遍历时,如果已经遍历完v所能直接到达的顶点而low[v]=dfn[v],我们知道v一定能到达栈里v上面的顶点,这些顶点的low一定小于 自己的dfn,不然就会出栈了,也不会小于dfn[v],不然low [v]一定小于dfn[v],所以栈里v以其v以上的顶点组成的子图是一个强连通分量,如果它不是极大强连通分量的话low[v]也一定小于dfn[v](这里不再详细说),所以栈里v以其v以上的顶点组成的子图是一个极大强连通分量。
- 若存在边<i, j>且遍历到它的时候j在栈中,那么i和j可能存在三种关系:
- (1)i是j的祖先;
- (2)j是i的祖先;
- (3)i和j无前后关系。对于情况(1),必有dfn[j]>dfn[i],因此不必考虑;
对于情况(2),<i, j>是逆向边,显然i、j处于同一个强连通分支;
对于情况(3):<i, j>是横叉边。显然i、j必然在同一棵搜索树中(因为搜索树的根结点肯定满足low=dfn),设p=LCA(i, j),由于从p到j的路径上木有low=dfn的结点(否则j已经出栈了),所以j必然可以到达p,又因为p可以到达i,所以j也可以到达i,又因为存在边<i, j>,所以i、j处于同一个强连通分支,这样就需要在计算low[i]的时候把dfn[j]考虑进去,而不能让i及其所有后代成为一个强连通分支。
C++代码
/* 寻找有向图强连通分量的tarjan算法 * index表示的就是时间戳 * scc_cnt 表示强连通分量的个数 * belong[u] 表示结点u属于那一个强连通分量 * inst[u] 表示结点u是否仍然在栈中 * st[] 和 top 分辨表示栈和栈顶位置 *index = scc_cnt = top = 0*/ void targin(int u) { int v; dfn[u] = low[u] = ++index; st[++top] = u; inst[u] = 1; for (int i = head[u];i != -1;i = edge[i].next) { v = edge[i].v; if (!dfn[v]) { targin(v); low[u] = min(low[u],low[v]); } else if (st[v]) low[u] = min(low[u],dfn[v]); } if (low[u] == dfn[u]) { scc_cnt++; do { v = st[top--]; inst[v] = 0; belong[v] = scc_cnt; } while (u != v); } }