分类: 算法

有向图强连通分支:Kosaraju’s algorithm

有向图强连通分支算是个基础算法,不过总是忘记,写下来备忘。

无向图强连通分支非常简单,使用图的遍历算法(DFS或BFS)即可,而有向图的强连通分支计算则要复杂一些,Kosaraju’s algorithm实现了\(O(n+m)\)时间复杂度的有向图强连通分支算法。

算法的核心思想在于:从有向图中任何一个点出发做DFS,必然能从图中“拖”出一个点集,和无向图中不同的是,这个点集不一定构成强连通分支,但是如果我们能通过一个合适的顺序进行DFS(“sink” vertex),则可以依次把每一个强连通分支“拖”出来,得到正确的结果,那么算法的要点则在于如何寻找这个合适的顺序。

算法通过两次DFS来求解,步骤如下:

  1. 对原图\(G\)做反转(reverse)操作,即将所有有向边逆置,得到图\(G^{rev}\)
  2. 在图\(G^{rev}\)上做后序的DFS,得到点遍历顺序
  3. 按照上一步中得到的遍历顺序,从大到小在原图\(G\)中通过DFS依次“拖”出强连通分支

运行过程如下图例,\(f(v)\)代表点在\(G^{rev}\)的遍历结束时间,从9号点开始DFS。

Exampl  e execution of the strongly connected components algorithm.

Exampl e execution of the strongly connected components algorithm.

记得刚学习这个算法时一直有的一个疑惑:为什么在第一遍DFS时一定要在图\(G^{rev}\)上做么?难道不能通过在原图\(G\)上DFS的顺序从小到大的进行第二次DFS么?仔细研究算法正确性的证明,不难发现这个想法是错误的。

该算法正确性证明的核心在于对于图\(G\)中任意两个相邻的强连通分支\(C_1\)和\(C_2\)且存在边\((i,j)\)满足\(i \in C_1 \land j \in C_2\)(在强连通分支的DAG中方向为\(C_1 \to C_2\)),可以证明:

$$\displaystyle \max_{v\in C_1}f(v) < \max_{v\in C_2}f(v)$$

\(f(v)\)代表点在\(G^{rev}\)的遍历结束时间,因此,在图G中具有最大\(f(v)\)的点一定为“sink” vertex。

那么现在来看看之前的想法错在哪里了,令\(f'(v)\)代表点在\(G\)的遍历结束时间,如果按照从小到大的顺序在\(G\)可以依次“拖”出强连通分支,则我们需要证明(强连通分支\(C_1\)和\(C_2\)的定义同上):

\(\displaystyle \min_{v\in C_1}f(v) > \min_{v\in C_2}f(v)\)

显然这是不对的…反例见下图中强连通分支{9,6,3}和{8,5,2},\(f'(3)=1\)比{8,5,2}中的完成时间都要小,如果从该点开始“拖”强连通分支得到的是错误的结果{9,6,3,8,5,2}。

遍历顺序

遍历顺序

所以\(G^{rev}\)的计算是必不可少的。

代码如下,采用递归实现DFS,在实际使用中容易造成栈溢出,修改为非递归实现即可。

好吧,我承认我就是为了凑数的…


参考资料

  • Algorithms: Design and Analysis, Part 1 by Tim Roughgarden on Coursera