强连通分量的三种算法

常见的(我见过的)强连通分量的三种算法有:1. Kosaraju算法(双DFS)2.Tarjan算法 3.Gabow

一.Kosaraju算法

算法的核心实现是,首先DFS一遍,得到一个DFS森林,在此过程中得到所有点的拓扑序列(按结束时间由高到低),之后我们建一个反向图,按反拓扑序(结束时间由高到低)进行第二次DFS,则此时得到的每一棵树都是一个强连通分量,这个画个图演示一下比较好理解,严格证明还是参考算法导论340页较好,感性的认识是,假定我们有C1,C2两个强连通分量,而在反拓扑序中C1是在C2前面的,此时说明G中第一遍DFS时先结束了C2,C1才结束的,假设C2中的点是从C1可达的,也就是在第一遍DFS时C2中点的全部时间戳的区间位于C1的区间内,则反向时,这时当将所有边反转时C1就没有边能到C2了(前提是他俩确实是SCC),按照先C1在C2的顺序DFS就行了,这里用到了一些DFS的性质,看算法导论为好。如果它们本来就是两棵树那么饭拓扑序靠前的那棵也不可能有边到后面的。虽然本算法慢,但是它得到了一个连通分量的拓扑序,有时用的上。

二.Tarjan算法

    Tarjan算法时间效率比Kosaraju好,因为算法中只使用了一次DFS,实现的思想是,我对每个点规定3个属性tim[i]和low[i]和vst[i], tim和DFS中的时间戳类似,就是只记录到达时间,low反映的是当前点通过自己的非树边(回退边)或子树中的非树边(回退边),最多能连到离根最近的点(本点须为当前点的直系祖先或当前点),在DFS到此点初始化时令tim[v] = low[v] =   ++ times,这样当DFS完一个点的子树之后,发现tim[v]仍然和low[v]相等,就说明这个点和它的子树中的点组成了一个强连通分量了,在这个过程中我们可以用一个栈来辅助,在DFS一个点时就压入这个点,同时令标记inStack[i] = true,这样我们可以通过Stack来判断v点邻接中的已访问过的点是不是v的直系祖先(是直系祖先才能构成环)。在DFS完一个点后,判断tim[v]是否还与low[v]相等,相等的话说明这个点与他的子树构成了一个强连通分量。弹出stack中的点,直到本点,赋予他们同一个强连通分量标号,同时令inStack[v] = false;

我自己利用并查集实现了一下Tarjan算法,过程类似于用并查集实现双连通分量,不过因为是无向图,当前的一条分枝可能还是会指向原来已经DFS过的sbling旁支,这时我们的解决方案是用DFS的双时间标号标记起始和终止时间,根据DFS标号的区间性质就可以判v邻接的一个已访问过的点是否是直系祖先了。并查集的实现过程是一旦son没被访问且low[son] >= tim[father],merge两者。

三.Gabow

    其实如果说能把Gabow也单列出来作为一种算法,我那个也能算一种了,区别在于我的比Tarjan原版还慢,而Gabow好像快一点,Gabow算法使用两个栈来维护,Stk1和原来的一样,stk2则用来求强连通分量构成子树的根节点,插入过程stk1,stk2和Tarjan中的一样,遇到一个插入一个,而当有一个点的邻接点是他的祖先在栈中我们就弹出stk2中的点直到stk2[top] == 这个邻接的点,这时剩下的这个点很有可能是一个强连通分量的根节点,DFS完一个点后,看stk2[top]是否等于当前节点,若等于同样说明当前点所有子树中点最多回退到当前点,此时构成了一个强连通分量,stk2中弹出这个点,stk1操作与Tarjan中相同,对每个标记强连通分量标号即可。这样的好处是不用频繁修改low的值了,好像能快一点,实现也没比Tarjan复杂,基本一样。

HOJ 2741实测结果

1.Gabow: 0.19s

2.Tarjan:0.20s

3.Tarjan + Union-Find Set :0.71s

posted @ 2011-08-07 21:43  zqynux  阅读(684)  评论(0编辑  收藏  举报