內容概要:
- 基於深度優先後序遍歷的DAG圖拓撲排序
- 強連通分量
- 求解強連通分量Kosaraju算法
拓撲排序的另一種方式
求解強連通分量前,來看與之相關的一個求拓撲排序的算法,這個算法基於深度優先後序遍歷,所謂深度優先後序遍歷,就是在深度優先遍歷過程中要在遍歷完一個節點的所有相鄰節點後才遍歷該節點。
當然DFS後序也不一定是唯一的。
基於DFS的拓撲排序算法描述
深度優先後序遍歷的逆序就是一個DAG圖的拓撲排序結果,這很好理解,DFS後序遍歷中後遍歷到的一定是先遍歷到的節點的前驅。但當圖不是DAG圖時,我們也能得到這樣的一個DFS後序遍歷序列以及它的逆序,但這已經不是拓撲排序了,所以該算法不能做環檢測。
算法實現
利用實現過的環檢測類和DFS類。使用環檢測類因爲拓撲排序只對DAG圖有意義,但該算法本身不能進行環檢測。
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.Queue;
public class TopoSortPostDFS {
private Graph G;
private ArrayList<Integer> res;
private boolean hasCycle = false;
public TopoSortPostDFS(Graph G){
if(!G.directed)
throw new IllegalArgumentException("TopoSort only works in directed graph!");
this.G = G;
res = new ArrayList<>();
hasCycle = (new DirectedCycleDetection(G)).hasCycle();// 有環則無拓撲序
if(hasCycle) return;
GraphDFS dfs = new GraphDFS(G);
for(int v: dfs.post())
res.add(v);
Collections.reverse(res);
}
public boolean hasCycle(){
return hasCycle;
}
public ArrayList<Integer> result(){
return res;
}
public static void main(String args[]){
Graph g = new Graph("g2.txt", true);
TopoSortPostDFS ts = new TopoSortPostDFS(g);
System.out.println(ts.result());
}
}
強連通分量
在有向圖中,如果兩個頂點間有一條從到的有向路徑,同時還有一條從到的有向路徑,則稱兩個頂點強連通。如果有向圖的每兩個頂點都強連通,稱G是一個強連通圖。有向圖的極大強連通子圖,稱爲的強連通分量(Strongly Connected Components)。
在上圖中,不同的顏色就對應一個強連通分量。
強連通分量求解思路
圖G的強連通分量中,同屬一個強連通分量的頂點彼此可達,那麼如果將每個強連通分量整體看做一個點,就可以得到一個新的抽象有向圖。
新的有向圖一定是一個DAG圖,因爲兩個不同強連通分量之間的頂點一定不彼此可達,否則它們應該構成環,這樣它們會屬於同一個強連通分量,矛盾。
基於上述思想,如果對新的DAG圖按照DFS後序遍歷,那麼就可以得到新圖的拓撲排序的逆序,對應到原圖,每個抽象的點就是該點代表的強連通分量。下面要解決的就是如何保證原圖的遍歷序列一定是連通分量依次遍歷的結果,顯然單純的DFS後序遍歷不一定能做到這一點,如下圖,從0開始DFS後序遍歷,2 3 1 0 是一個正確的序列,但不是按連通分量的遍歷順序,我們希望得到的順序是3 2 1 0這樣的。
解決辦法就是將原圖進行翻轉(每條邊變反向),得到對應的反圖,這就是Kosaraju算法的核心。
Kosaraju算法
定理:按反圖DFS後序遍歷序列的逆對原圖進行DFS得到的是原圖強連通分量拓撲排序的逆(忽略頂點順序在一個強連通分量看做一個點的抽象圖中)。
如在上圖中,從0開始,反圖的DFS後序序列爲1 2 0 3,其逆序爲3 0 2 1,這樣按照3 0 2 1的順序對原圖進行DFS就可以得到不同的連通分量:3和0 1 2。爲了進一步解釋正確性,現在再將原圖看做反圖的反圖,從0開始,2 3 1 0 是原圖的一個DFS後序序列,其逆序爲 0 1 3 2,按照0 1 3 2順序對反圖進行DFS就可以得到不同的連通分量:0 1 2 和3。
利用上述定理,Kosaraju算法只需要求一個圖的反圖,再按照反圖的DFS後序遍歷序列的逆序進行DFS即可。由於反圖與原圖的強連通分量一樣,所以對原圖進行DFS後序遍歷,再按照原圖的DFS後序遍歷序列的逆序對反圖進行DFS也可以。
理解Kosaraju算法的關鍵是,每個強連通分量中的點在 反圖DFS後序序列的逆 中不一定是連續排列的,但 反圖DFS後序序列的逆 中,每個連通分量至少有一個點排在前面,這樣再按照DFS進行訪問一定會遍歷整個強連通分量。
Kosaraju算法實現
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
public class SCC {
private Graph G;
private int[] visited; // 標記頂點是否被訪問以及屬於哪個連通分量
private int scccount = 0; // 求連通分量個數
public SCC(Graph G){
this.G = G;
visited = new int[G.V()];
Arrays.fill(visited, -1);
GraphDFS dfs = new GraphDFS(G.reverseGraph());
ArrayList<Integer> order = new ArrayList<>();
for(int v: dfs.post())
order.add(v);
Collections.reverse(order);
for(int v : order)
if(visited[v] == -1) {
dfs(v, scccount);
scccount ++; // dfs(v, scccount ++);
}
}
private void dfs(int v, int ccid){// ConnectedComponent ID
visited[v] = ccid;
for(int w: G.adj(v))
if(visited[w] == -1)
dfs(w, ccid);
}
public int count(){
return scccount;
}
public ArrayList<Integer> getCC(){// 查看連通分量標記
ArrayList<Integer> cc = new ArrayList<>();
for(int i = 0; i < visited.length; i ++)
cc.add(visited[i]);
return cc;
}
public ArrayList<Integer>[] components(){
// 返回各個強連通分量
ArrayList<Integer>[] res = new ArrayList[scccount];
for(int i = 0; i < scccount; i ++)
res[i] = new ArrayList<>();
for(int v = 0; v < G.V(); v ++)
res[visited[v]].add(v);
return res;
}
public boolean isStronglyConnected(int v, int w){
// 判斷兩個頂點是否互相可達
G.validateVertex(v);
G.validateVertex(w);
return visited[v] == visited[w];
}
public static void main(String args[]){
Graph g = new Graph("g2.txt", true);
SCC cc = new SCC(g);
System.out.println(cc.count());
System.out.println(cc.getCC());
ArrayList<Integer>[] comp = cc.components();
for(int ccid = 0; ccid < comp.length; ccid ++){
System.out.print(ccid + ": ");
for(int w: comp[ccid])
System.out.print(w + " ");
System.out.println();
}
}
}