参照《算法》第四版书上相关内容
注:有向图取反后与原有向图的连通性相同。
Kosaraju算法用来解决有向图的连通性问题,算法的基本步骤:
1.对一幅有向图G,计算它的反向图GR的逆后序排列(一次dfs)。
2.按由1计算得到的顺序对G进行dfs操作,来访问所有未被标记的顶点。
3.在构造函数中,所有在同一个递归dfs()调用中被访问到的顶点都在一个强连通分量中。
疑惑在于:第一步骤为何对G进行反向后取逆后序排列?
以下图为例进行分析:
分析:若按标准顺序(0~V-1,也可按任意顺序)进行dfs,如按A~K顺序依次检查,那么递归顺序为A->B(->C)->D->E->F,结果显示ABC与DEF为同一连通分量,结果为错。错误的根源在于存在B->D这条边,而G->F这条边却并无影响。究其原因,是(1)连通分量dfs操作发生在(2)连通分量之前,因而运行到dfs(B)时,递归会进入D(即(2)连通分量中),(而实际上应该是在D处从头开始递归,count++),最终会导致(1)与(2)连通分量误并为同一连通分量。
解决方法:通过一些操作让本应该先递归的分量放在后面检查,如先对(2)连通分量进行dfs,标记后,再从(1)连通分量进行检查dfs,当执行到B处时,虽然有B->D这条边,但是D已经被标记过,则将结束(1)连通分量的检查。
现在就可以把问题抽象成为:如何安排(1)(2)(3)(4)的检查顺序(而与(1)(2)(3)(4)的内部顺序无关),使连通分量的检查成立。
这就是一个有优先级顺序的排序问题(拓扑排序解决)。优先级的限制条件为(2)应该在(1)(3)之前被dfs,(4)在(3)之前dfs(否则会产生上述问题)。
操作:原有向图进行拓扑排序后为(1)(3)在(2)之前运行,(3)在(4)之前运行,所以将原有向图取反后拓扑(即与原有向图结果相反),就会满足优先级限制条件:(2)在(1)(3)之前运行,(4)在(3)之前运行。
对此,算法的步骤一中进行有向图的反向后取逆后序进行了解答。
下面是Kosaraju算法的具体实现代码:
package 有向图.有向图中的强连通性;
import 有向图.图的基本定义.Digraph;
import 有向图.基于深度优先搜索的顶点排序.DepthFirstOrder;
/**
* 强连通分量的API:
* boolean stronglyConnected(int v,int w) v和w是否是强连通的
* int count() 图中的强连通分分量的总数
* int id(int v) v所在的强连通分量的标识符(0~count-1)
*/
public class KosarajuSCC {
private boolean[] marked; //已访问过的顶点
private int[] id; //强连通分量的标志符
private int count; //强连通分量的数量
public KosarajuSCC(Digraph G){
marked = new boolean[G.V()];
id = new int[G.V()];
DepthFirstOrder order = new DepthFirstOrder(G.reverse()); //取反向图
for(int s : order.reversePost()){ //逆后序排列
if(!marked[s]){
dfs(G,s);count++;
}
}
}
private void dfs(Digraph G,int v){
marked[v] = true;
id[v] = count;
for(int w : G.adj(v)){
if(!marked[w]){
dfs(G,w);
}
}
}
public boolean stronglyConnected(int v,int w){
return id[v] == id[w];
}
public int id(int v){
return id[v];
}
public int count(){
return count;
}
}
补充:
拓扑排序:给定一幅有向图,将所有的顶点排序,使得所有的有向边均从排在前面的元素指向排在后面的元素。
一幅有向无环图的拓扑排序即为所有顶点的逆后序排列。