Kosaraju算法
今天在学习图论算法的过程中,接触到了一个挺有意思的算法;
首先,先明确一下强连通分量的定义:如果两个顶点v和w是互相可达的,则称它们是强连通的(两个顶点是强连通的当且仅当它们都在一个普通的有向环中);
接下来,不妨先来思考下面这两个问题:
- 给定一幅有向图,给定的两个顶点是强连通的吗?
- 给定一幅有向图,这副有向图中的强连通分量有多少个?
算法过程
通过上述的思考,我们可能对于Kosaraju算法有一个大致的了解,那么算法的实现思路又是怎么样的呢?
-
首先,我们先对原图G进行一遍DFS,同时将访问顶点的记录压入栈中(从栈中依次弹出的顶点顺序为该有向图的拓扑排序);
-
其次,从栈顶依次弹出顶点,在原图的反图(下图)中,按照拓扑排序的顺序,对反图G’ 进行DFS;
-
此时,在外循环中进入反图DFS的次数便为有向图的强连通分量的个数;
细节分析
相信通过上述的算法描述,您应该对于该算法有了一个宏观的了解,相信在您的脑海中会有几个问题:
- 为什么要将顶点访问记录压入栈?
- 为什么又要对反图G’ 按照栈中的访问记录进行DFS?
那么,接下来就是对于上述问题的分析:
Q1: 为什么要将顶点访问记录压入栈?
我们对原图进行DFS,将顶点的访问记录压入栈,其目的是为了得到该有向图顶点的拓扑排序(依次出栈即可得到);然而拓扑排序的意义非常明确,就类似于大学内的课程都要有其先学课程一样,我们可以得到一张类似的“大学课程学习顺序表”;不同的强连通分量就好比不同的”专业课程“,我们的目标就是寻找有多少个不同的专业;
Q2:为什么又要对反图G’ 按照栈中的访问记录进行DFS?
反图的作用是可以将本来连通(非强连通)的强连通分量,按照原图的访问顺序无法再连通,从而将各个强连通分量分离出来(站在原图的访问顺序上来看); 我们现在已经得到了有向图中的顶点访问顺序表(类似于“大学课程学习表”),那么我们就从“每个专业”最基础的 “课程” 开始,相同专业的课程会在一次DFS中被标记,因此,我们最终通过计数进入第二次DFS的次数便可得到该有向图中的强连通分量的个数;
代码部分(求解上图的强连通分量个数)
DiGraph(顶点的邻接集可以使用List来代替,这里使用Bag是出于安全性来考虑):
public class DiGraph {
private final int V;
private int E;
private Bag<Integer>[] adj;
public DiGraph(int V) {//读取构造
this.V = V;
this.E = 0;
adj = (Bag<Integer>[]) new Bag[V];
for (int v = 0; v < V; v++) {
adj[v] = new Bag<>();
}
}
public int V() {
return V;
}
public int E() {
return E;
}
public void addEdge(int v, int w) {//由v--->w的一条路径
adj[v].add(w);
E++;
}
public Iterable<Integer> adj(int v) {
return adj[v];
}
public DiGraph reverse() {//求出反向图
DiGraph R = new DiGraph(V);
for (int v = 0; v < V; v++) {
for (int w : adj(v)) {
R.addEdge(w, v);
}
}
return R;
}
@Override
public String toString() {
String S = V + " 顶点, " + E + " 边\n";
for (int v = 0; v < V; v++) {
S += v + ": ";
for (int w : this.adj(v)) {
S += w + " ";
}
S += "\n";
}
return S;
}
}
Kosaraju:
public class Kosaraju {
public static void main(String[] args) {
DiGraph G = new DiGraph(9);
G.addEdge(0, 1);
G.addEdge(1, 2);
G.addEdge(2, 0);
G.addEdge(0, 3);
G.addEdge(3, 4);
G.addEdge(4, 6);
G.addEdge(4, 5);
G.addEdge(6, 5);
G.addEdge(5, 3);
G.addEdge(4, 7);
G.addEdge(7, 8);
G.addEdge(8, 7);
Kosaraju koa = new Kosaraju(G);
System.out.println(koa.getCount());
}
private Set<Integer> visited;
private Stack<Integer> order;
private boolean[] marked;
private int count; //强连通分量的数量
public Kosaraju(DiGraph G) {
visited = new HashSet<>();
order = new Stack<>();
marked = new boolean[G.V()];
//获取顶点遍历的逆后续
for (int i = 0; i < G.V(); i++) {
if (!visited.contains(i)) topological_dfs(G, i);
}
//到这里order已经包含(出栈顺序)对反图进行dfs遍历的顺序
DiGraph revG = G.reverse();
for (int i = 0; i < G.V(); i++) {
if (!marked[i]) {
get_dfs(revG, i);
count++;//强连通分量的个数
}
}
}
public void topological_dfs(DiGraph G, int v) {
visited.add(v);
for (int w : G.adj(v)) {
if (!visited.contains(w)) topological_dfs(G, w);
}
order.push(v);
}
public void get_dfs(DiGraph G, int v) {
marked[v] = true;
for (int w : G.adj(v)) {
if (!marked[w]) get_dfs(G, w);
}
}
public int getCount() {//获取强连通分量
return count;
}
}