有向圖
Introduction
就是邊是有方向的,像單行道那樣,也有很多典型的應用。

點的出度指從這個點發出的邊的數目,入度是指向點的邊數。當存在一條從點 v 到點 w 的路徑時,稱點 v 能夠到達點 w ,但要注意這並不意味着點 w 可以到達點 v 。

Digraph API
先給出表示有向圖的 API 以及簡單的測試用例,booksite-4.2 上可以找到完整的。
API

Sample Client

運行示例

仍然使用鄰接表來實現有向圖,比無向圖還簡單其實。
Adjacency-list

Java Implementation
public class Digraph {
private final int V;
private final Bag<Integer>[] adj; // adjacency lists
public Digraph(int V) {
this.V = V;
// create empty graph with V vertices
adj = (Bag<Integer>[]) new Bag[V];
for (int v = 0; v < V; v++) {
adj[v] = new Bag<Integer>();
}
}
// add edge v->w
public void addEdge(int v, int w) {
adj[v].add(w);
}
// iterator for vertices pointing from v
public Iterable<Integer> adj(int v) {
return adj[v];
}
}
用鄰接表來表示有向圖,類似的內存使用正比於 E+V ,常數時間就能加入新邊,判斷點 v 到 w 是否有條邊需要正比於點 v 出度的時間,遍歷從點 v 發出的邊也是。
Digraph Search
同樣的,可以直接用 Undirected Graphs 提到的 DFS 和 BFS 這兩種搜索策略。
DFS
public class DirectedDFS {
private boolean[] marked; // true if path from s
// constructor marks vertices reachable from s
public DirectedDFS(Digraph G, int s) {
marked = new boolean[G.V()];
dfs(G, s);
}
// recursive DFS does the work
private void dfs(Digraph G, int v) {
marked[v] = true;
for (int w : G.adj(v)) {
if (!marked[w]) {
dfs(G, w);
}
}
}
// client can ask whether any vertex is reachable from s
public boolean visited(int v) {
return marked[v];
}
}
示例

BFS
隨便來張圖感受一下。

Topological Sort
拓撲排序。就是把有向圖整理成箭頭都朝同一個方向,像把下面的中間變成右邊那種。應用也很廣泛啦,比如說大學里某些課要上先修課才行,拓撲排序就可以幫我們安排課程順序。

另外,拓撲排序是針對有向無環圖(DAG, directed acyclic graph)來說的,設想 a 的完成依賴於 b,b 的完成又依賴於 a ,顯然沒有解。
Solution
DFS 稍作修改,就可以幫我們完成拓撲排序。因為 DFS 正好只會訪問每個頂點一次,如果將 dfs() 參數之一的點保存在一個數據結構中,遍歷這個數據結構實際上就能訪問圖中的所有頂點,遍歷的順序取決於這個數據結構的性質以及是在遞歸調用之前還是之后進行保存。在典型的應用中,人們感興趣的是點的以下 3 種排列順序。
- 前序(Preorder):在遞歸調用之前將點加入隊列。
- 后序(Postorder):在遞歸調用之后將點加入隊列。
- 逆后序(Reverse postorder):在遞歸調用之后將點壓入棧。
前序就是 dfs() 的調用順序,后序就是點遍歷完成的順序,而逆后序就是拓撲排序。
樣圖

模擬

前序和后序看上圖感受一下,對於逆后序就是拓撲排序可以這么想:對於任意邊 v->w,在調用 dfs(v) 之時,可能的情況有三種。
- dfs(w) 已經被調用過且返回了(w 已經被標記)。
- dfs(w) 還沒有被調用(w 還未被標記),因此 v->w 會直接或間接調用並返回 dfs(w),且 dfs(w) 會在 dfs(v) 返回前返回。
- dfs(w) 已經被調用但還未返回。證明的關鍵在於,在 DAG 中這種情況是不可能出現的,這是由於遞歸調用鏈意味着存在從 w 到 v 的路徑,再加上現在的邊 v->w 則剛好補成一個環。
所以在 DAG 中只可能有前面兩種情況,其中 dfs(w) 都會在 dfs(v) 之前完成,也就是說后序排序中 v 指向的點都會在其前面,那么逆后序就是把 v->w 中的 w 排在 v 后面啦。具體實現把 DFS 改一下就好,加些數據結構存點,完整示例可以參見:DepthFirstOrder.java。
Directed Cycle Detetion
上面提到過,當且僅當有向圖沒有有向環時,它才有拓撲排序,DFS 也能用於檢測圖是否含有環。因為系統維護的遞歸調用的棧表示的正是“當前”正在遍歷的有向路徑,如果我們遇到了一條邊 v->w ,而 w 已經在棧里,就找到了一個環 v->w->v。
Implementation
public class DirectedCycle {
private boolean[] marked;
private int[] edgeTo;
private Stack<Integer> cycle; // 有向環中的所有頂點(如果存在)
private boolean[] onStack; // 遞歸調用的棧上的所有頂點
public DirectedCycle (Digraph G) {
onStack = new boolean[G.V()];
edgeTo = new int[G.V()];
marked = new boolean[G.V()];
for (int v = 0; v < G.V(); v++) {
if (!marked[v]) {
dfs(G, v);
}
}
}
private void dfs(Digraph G, int v) {
onStack[v] = true; // 遞歸調用開始時標記為在棧中
marked[v] = true;
for (int w : G.adj(v)) {
if (this.hasCycle()) {
return;
}
else if (!marked[w]) {
edgeTo[w] = v;
dfs(G, w);
}
// v->w 中 w 已經在棧中,保存環 v->w->...->v到 cycle 里
else if (onStack[w]) {
cycle = new Stack<Integer>();
for (int x = v; x != w; x = edgeTo[x]) {
cycle.push(x);
}
cycle.push(w);
cycle.push(v);
}
onStack[v] = false; // 遞歸調用結束時標記為不在棧中
}
}
public boolean hasCycle() {
return cycle != null;
}
public Iterable<Integer> cycle() {
return cycle;
}
}
Strong Components
強連通。在有向圖中,若同時存在路徑 v->w 和 w->v,則稱點 v 和 w 是強連通的。類似的,這顯然也是一個等價關系,滿足:
- symmetric: 自反性, v 和 v 自身是強連通的。
- reflexive: 對稱性, v 和 w 強連通,則 w 和 v 強連通。
- transitive: 傳遞性,如果 v 和 w 強連通,又有 w 和 x 強連通,那么 v 和 x 強連通。
強連通分量也很好理解,就是區域里面的點之間都是強連通的。
Demo

強連通分量可以幫助生物學家理解食物鏈中能量的流動,幫助程序員組織程序模塊等。

在 Undirected Graphs 中提到的連通分量問題,可以用 DFS 預處理圖,然后就可以在常數時間回應查詢。同樣的強連通問題也可以用 DFS 解決,用 kosaraju-sharir 算法,分成兩步用兩次 DFS。算法思想是計算核心 DAG (把強連通分量當成一個點)的拓撲排序,再按逆拓撲序列對點跑 DFS。我也不知道在說什么,看下面證明。

Phase 1
用 DFS 計算圖 G 的反向圖 \(G^{R}\) (邊的方向相反)的逆后序序列。

Phase 2
按第一步中的逆后序序列來對圖 G 進行 DFS 。

證明
分兩點證明該算法的正確性。
-
第二步構造函數中調用的 dfs(G, s) 會訪問每個和 s 強連通的點
反證法。假設某個和點 s 強連通的點 v 沒有在 dfs(G, s) 中被訪問,那就意味着 marked[v] 為 true,即點 v 在 s 之前就已經被訪問過了。又因為兩點強連通,故存在着從 v 到 s 的路徑,所以訪問 v 的時候的 dfs(G, v) 就會調用 dfs(G, s),而不會輪到構造函數來調用。矛盾,得證。
-
構造函數調用的 dfs(G, s) 所到達的任意點 v 都必然和 s 強連通
v 能被 dfs(G, s) 訪問到,說明存在路徑 s->v ,那么只要再證明存在路徑 v->s 就能說明兩點是強連通的。即等價於在 \(G^{R}\) 中找路徑 s->v,且是在已知存在路徑 v->s 的前提下。因為在 \(G^{R}\) 的逆后序中 v 排在 s 后面,所以 dfs(G, v) 結束得比 dfs(G, s) 早,那就只有兩種情況:
- 調用 dfs(G, v) 結束在調用 dfs(G, s) 開始之前。
- 調用 dfs(G, v) 開始在 dfs(G, s) 調用開始之后且結束在 dfs(G, s) 結束之前。
又因為已知存在路徑 v->s ,所以第一種情況是不可能的,而第二種則意味着存在路徑 s->v,證畢,再來張沒什么大用的圖。

實現
實現只要對 Undirected Graphs 中的 “Implementation With DFS” 代碼稍作修改就好其實。
public class KosarajuSharirSCC {
private boolean[] marked;
private int[] id;
private int count;
public KosarajuSharirSCC(Digraph G) {
marked = new boolean[G.V()];
id = new int[G.V()];
DepthFirstOrder dfs = new DepthFirstOrder(G.reverse());
// 按逆后序進行 DFS
for (int v : dfs.reversePost()) {
if (!marked[v]) {
dfs(G, v);
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];
}
}
