1. 程式人生 > >求解強連通分量演算法之---Kosaraju演算法

求解強連通分量演算法之---Kosaraju演算法

本文提綱:

  • 問題描述
  • Kosaraju 演算法

問題描述:

什麼是強連通分量(StronglyConnected Component)(或者,被稱為強連通子圖,Strongly Connected Subgraph)?

首先需要明白的是,強連通分量只可能存在於有向圖中,無向圖中是不存在強連通分量的,當然,無向圖中也有對應物,被稱為連通分量(Connected Component),求解無向圖中的連通分量,根據具體要求,可以選擇使用並查集或者DFS


看一張取自wiki的圖就明白什麼是強連通分量了:


以上用虛線圍繞的部分就是一個強連通分量,因此上圖中總共含有三個。

對於一個強連通分量中的任意一對頂點

(uv),都能夠保證分量中存在路徑使得u->vv->u

比如上圖中由abe這三個頂點構成的分量中,任意兩個頂點間都存在路徑可達。

順便也介紹一下有關“縮點”的概念:

由於強連通分量的特殊性,在一些實際應用中,會將每個強連通分量看成一個點,然後進行處理。這樣做主要是為了降低圖的複雜度,特別是在強連通分量規模大、數量多的情況中,利用“縮點”能大幅度降低圖的複雜度。

縮點後得到的圖,必定是DAG。用反正能夠很方便的進行證明:因為若圖中含有環路,即意味著至少有兩個點彼此可達,那麼按照強連通分量的定義,這兩個點應該屬於一個分量中,因而在縮點發生後,會被一個點所代表。由此推匯出矛盾。比如,對上圖進行縮點處理,最後的結果就是:

(abc) ->a'(fg) ->b'(cdh) ->c'

因此最後的圖就可以表示為:


更加具體,更加嚴謹的表述,可以參考:

Kosaraju 演算法

首先摘錄一段wiki上的演算法過程描述:

  1. Let G be a directed graph and S be an empty stack.
  2. While S does not contain all vertices:
    1. Choose an arbitrary vertex v not in S. Perform a depth-first search starting at v. Each time that depth-first search finishes expanding a vertex u, push u onto S.
  1. Reverse the directions of all arcs to obtain the transpose graph.
  1. While S is nonempty:
    1. Pop the top vertex v from S. Perform a depth-first search starting at v. The set of visited vertices will give the strongly connected component containing v; record this and remove all these vertices from the graph G and the stack S. Equivalently, breadth-first search (BFS) can be used instead of depth-first search.

仔細觀察步驟2-a,可以發現,這個過程和使用基於DFS的拓撲排序中新增頂點到最終結果棧中的過程幾乎一致。(請參考這裡)只不過這裡的圖不一定是DAG,而拓撲排序中的圖一定是DAG

在這裡順便提一下在呼叫dfs的過程中,幾種新增頂點到集合的順序。一共有四種順序:

  • Pre-Order,在遞迴呼叫dfs之前將當前頂點新增到queue
  • Reverse Pre-Order,在遞迴呼叫dfs之前將當前頂點新增到stack
  • Post-Order,在遞迴呼叫dfs之後將當前頂點新增到queue
  • Reverse Post-Order,在遞迴呼叫dfs之後將當前頂點新增到stack

最後一種的用途最廣,至少目前看來是這樣,比如步驟2-a以及拓撲排序中,都是利用的Reverse Post-Order來獲取頂點集合。

對於DAG,利用Reverse Post-Order最終獲取的集合就代表對該圖進行拓撲排序的結果,但是對於非DAG而言,這樣處理之後得到的是一種“偽拓撲排序”結果,為什麼這樣說,因為最終的結果不一定滿足拓撲排序中嚴格的偏序定義,比如對文中第一幅圖進行“偽拓撲排序“後的結果為可能為(結果不唯一,具體可以參考拓撲排序那篇文章中的相關部分)

(abecdhgf)

將結果和原圖對比,可以發現,結果不滿足偏序關係,因為存在迴向邊:

e->ad->ch->df->g

而這些迴向邊,就是構成強連通分量的關鍵。為了突出這些迴向邊,Kosaraju演算法的步驟三就是將圖進行轉置。轉置後,原來的迴向邊就都變成正向邊了:

a->ec->dd->hg->f

對轉置後的圖按照上面”偽拓撲排序“中頂點出現的順序(aebcdhgf)呼叫DFS,即步驟4

而每次呼叫DFS形成的一顆搜尋樹,就構成了原圖中的一個強連通分量。比如呼叫dfs(a),會呼叫dfs(e),緊接著後者有呼叫了dfs(b)。然後依次返回,因此abe就構成了一個強連通分量。依次類推,cdh以及gf也分別構成強連通分量。

回顧一下Kosaraju的主要步驟:

  1. G求解Reverse Post-Order,即上文中的”偽拓撲排序“
  2. G進行轉置得到GR
  3. 按照第一步得到的集合中頂點出現的順序,對GR呼叫DFS得到若干顆搜尋樹
  4. 每一顆搜尋樹就代表了一個強連通分量

坦率地說,這個演算法的想法很巧妙,為了突出迴向邊,而對圖進行轉置,然後對轉置的圖按照之前得到的頂點序列進行DFS呼叫。整個演算法的確能夠正確工作,但是總感覺怪怪的,確實,這個演算法不太好理解,儘管它的實現十分簡單直觀。


使用Java的實現程式碼:
public class KosarajuSCC {

	private Digraph digraph;

	private int V;

	private boolean[] visited;

	private int[] components;

	private List<List<Integer>> sccs;

	// record the current component id
	private int current = 0;

	// reverseTopo is not necessarily a topological order, it should be reverse
	// post order instead
	public KosarajuSCC(Digraph digraph, Iterable<Integer> reverseTopo) {
		this.digraph = digraph;
		V = digraph.getV();

		visited = new boolean[V];
		components = new int[V];

		for (int v : reverseTopo) {
			if (!visited[v]) {
				dfs(v);
				current++;
			}
		}
	}

	private void dfs(int v) {
		visited[v] = true;
		components[v] = current;

		for (int w : digraph.adj(v)) {
			if (!visited[w]) {
				dfs(w);
			}
		}
	}

	public int[] getComponents() {
		return components;
	}

	public List<List<Integer>> getSccs() {
		sccs = new ArrayList<List<Integer>>();

		for (int i = 0; i < current; i++) {
			sccs.add(new ArrayList<Integer>());
		}

		for (int i = 0; i < V; i++) {
			sccs.get(components[i]).add(i);
		}

		return sccs;
	}
}

下面對這個演算法的正確性進行證明:(如果沒有興趣,可以直接略過D)

證明的目標,就是最後一步 ---每一顆搜尋樹代表的就是一個強連通分量

證明:設在圖GR中,呼叫DFS(s)能夠到達頂點v,那麼頂點sv是強連通的。

兩個頂點如果是強連通的,那麼彼此之間都有一條路徑可達,因為DFS(s)能夠達到頂點v,因此從sv的路徑必然存在。現在關鍵就是需要證明在GR中從vs也是存在一條路徑的,也就是要證明在G中存在sv的一條路徑。

而之所以DFS(s)能夠在DFS(v)之前被呼叫,是因為在對G獲取ReversePost-Order序列時,s出現在v之前,這也就意味著,v是在s之前加入該序列的(因為該序列使用棧作為資料結構,先加入的反而會在序列的後面)。因此根據DFS呼叫的遞迴性質,DFS(v)應該在DFS(s)之前返回,而有兩種情形滿足該條件:

  1. DFS(v) START -> DFS(v) END -> DFS(s) START -> DFS(s) END
  1. DFS(s) START -> DFS(v) START -> DFS(v) END -> DFS(s) END

是因為而根據目前的已知條件,GR中存在一條sv的路徑,即意味著G中存在一條vs的路徑,而在第一種情形下,呼叫DFS(v)卻沒能在它返回前遞迴呼叫DFS(s),這是和G中存在vs的路徑相矛盾的,因此不可取。故情形二為唯一符合邏輯的呼叫過程。而根據DFS(s) START ->DFS(v) START可以推匯出從sv存在一條路徑。

所以從sv以及vs都有路徑可達,證明完畢。

複雜度分析:

根據上面總結的Kosaraju演算法關鍵步驟,不難得出,該演算法需要對圖進行兩次DFS,以及一次圖的轉置。所以複雜度為O(V+E)