参考
- GeeksforGeeks
如果所有的顶点对之间都有一条路径,那么一个有向图就是强连通的。有向图的强连通分量(strongly connected component,SCC)是一个最大强连通子图。例如,下面的图中有3个SCC。
我们已经讨论过Kosaraju的强连通分量的算法。前面讨论的算法需要对一个Graph进行两次DFS遍历。在这篇文章中,我们将讨论Tarjan算法,它只需要一次DFS遍历。
Tarjan算法基于以下事实:
- DFS搜索会产生一个DFS树/森林;
- 强连通分量构成DFS树的子树;
- 如果我们能找到这种子树的头,就可以打印/存储该子树的所有结点(包括头),这将是一个SCC;
- 从一个SCC到另一个SCC没有回溯边(back edge)(可以有交叉边,但在处理图的时候不会用到交叉边)。
为了找到一个SCC的头部,我们要计算disc和low数组(就像处理衔接点、桥、双连接组件一样)。如前几篇文章所讨论的,low[u]表示从以u为根的子树中可以到达的最早被访问的顶点(发现时间最短的顶点),如果disc[u]=low[u],则节点u为头部。
下图是该方法的说明:
强连通分量只与有向图有关,但Disc和Low值与有向图和无向图都有关。
在上图中,我们展示了一个图形和它的一颗DFS树(在同一个图形上可以有不同的DFS树,取决于边被遍历的顺序)。在DFS树中,连续的箭头是树的边,虚线的箭头是back edge
图中每个节点的disc和low的值显示为disc/low
Disc: 是指在DFS遍历时,节点第1次被访问的时间。这是在DFS遍历时,一个节点第一次被访问的时间。对于DFS树中的节点A,B,C,...,J,Disc值为1,2,3,…,10。
Low: 在DFS树中,树边带我们向前走,从祖先节点到它的一个子孙节点。例如,从节点C开始,树边可以带我们到节点G、节点I等。回溯边(back edge)带我们往后走,从一个子孙节点到它的一个祖先节点,例如,从节点G出发,back edge带我们到E或C。如果我们把树边和回溯边(back edge)一起看,那么我们可以看到,如果我们从一个节点开始遍历,我们可能会通过树边向下走,然后通过回溯边向上走。例如,从节点E开始,我们可以向下走到I或J,然后向上走到F。一个节点的Low值告诉了我们通过该节点的子树所能到达的最顶层祖先(最小可能的disc值)。所以对于任何一个节点,无论如何Low值都等于它的Disc值(一个节点是它自己的祖先)。然后我们查看它的子树,看看是否有任何节点可以带我们找到它的任何一个祖先。如果在子树中有多条回溯边可以带我们到不同的祖先,那么我们就选取Disc值最小的那条(即最上面的那条)。如果我们看一下节点F,它有两个子树。有节点G的子树,把我们带到E和C,另一个子树只把我们带回F。这里最顶层的祖先是F可以到达的C,所以F的Low值是3(C的Disc值)。
根据上面的讨论,应该很清楚,B、C、D的Low值是1(因为A是B、C、D可以到达的最上面的节点)。同理,E、F、G的Low值为3,H、I、J的Low值为6。
对于任何节点u,当DFS开始时,Low将会被设置为其Disc 1st。
然后对它的每一个子节点v依次进行DFS,u的Low值可以在两种情况下改变:
- Case1(Tree Edge):如果节点v还没有被访问,那么在v的DFS完成后,low[u] = min(low[u], low[v])
- Case2(Back Edge):当后代v已经被访问,那么low[u] = min(low[u], disc[v]);
在第二种情况下,我们是否可以用low[v]代替DIsc[v]?答案是NO。如果你能想到为什么答案是NO,那么你大概明白Low和Disc的概念。(因为是有向图,low[v]是v能访问到的最小节点,u不一定能访问到,u能访问到v,说明disc[v]一定是可以取到的)
相同的Low和Disc值有助于解决其他图的问题,如衔接点、桥和双连接分量。
为了跟踪以头部为根的子树,我们可以使用栈(在访问时不断推送节点)。当发现一个头部节点时,从堆栈中弹出所有节点,直到从栈中得到头部。
为了确保不考虑交叉边,当我们到达一个已经被访问过的节点时,只有当栈中存在被访问过的节点时,我们才应该对该节点进行处理,否则忽略该节点。
实现:写一个Tarjan算法找到有向图的所有连通分量并打印出来
1 | class Graph: |
1 | # Output |
时间复杂度:算法主要使用DFS,邻接表表示的图的DFS算法是O(V+E)复杂度的。