一.定義以及和無向圖的區別
一幅有向圖是由一組頂點和一組有方向的邊組成的,每條有方向的邊都連接着有序的一對頂點.有向邊是由第一個頂點指出並指向第二個頂點,用v->w來表示有向圖中一條由頂點v指向頂點w的一條邊.當存在從v->w的有向路徑的時候,稱頂點w能夠由頂點v達到.和無向圖不同的是,在有向圖中由v能夠到達w,並不意味着由w也能到達v.下圖為一個有向圖舉例.

二.有向圖的數據類型
使用Bag表示有向圖,其中邊v->w表示為頂點v所對應的鄰接鏈表中包含一個w頂點,與無向圖不同的是,這里每條邊只會出現一次.有向圖的數據結構類型如下:
/** * 有向圖的數據類型 * @author Administrator * */ 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<Integer>(); } } public int V() { return V; } public int E() { return E; } //添加一條邊v->w,由於是有向圖只要添加一條邊就可以了 public void addEdge(int v,int 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; } }
三.有向圖中的可達性
利用深度優先搜索可以解決有向圖中的單點可達性問題:即:給定一幅有向圖和一個起點s,回答是否存在一條從s到達給定頂點v的有向路徑的問題.
/** * 從指定的圖中查找從s可達的所有頂點. * 判斷一個點是否是從s可達的. * @author Administrator * */ public class DirectedDFS { private boolean[] marked; //從G中找出所有s可達的點 public DirectedDFS(Digraph G,int s) { marked=new boolean[G.V()]; dfs(G,s); } //G中找出一系列點可達的點 public DirectedDFS(Digraph G,Iterable<Integer> sources) { marked=new boolean[G.V()]; for(int s:sources) { if(!marked[s]) dfs(G,s); } } //深度優先搜素判斷. private void dfs(Digraph G, int v) { marked[v]=true; for(int w:G.adj(v)) { if(!marked[w]) dfs(G,w); } } //v是可達的嗎 public boolean marked(int v) { return marked[v]; } }
四.尋找有向環
下面的代碼可以用來檢測給定的有向圖中是否含有有向環,如果有,則按照路徑的方向返回環上的所有頂點.在執行dfs的時候,查找的是從起點到v的有向路徑,onStack數組標記了遞歸調用的棧上的所有頂點,同時也加入了edgeTo數組,在找到有向環的時候返回環中的所有頂點.
/** * 有向圖G是否含有有向環 * 獲取有向環中的所有頂點 * @author Administrator * */ public class DirectedCycle { private boolean[] marked; private int[] edgeTo; private Stack<Integer> cycle; //有向環中的所有頂點 private boolean[] onStack; //遞歸調用的棧上的所有頂點 public DirectedCycle(Digraph G) { edgeTo=new int[G.V()]; onStack=new boolean[G.V()]; marked=new boolean[G.V()]; for(int v=0;v<G.V();v++) { if(!marked[v]) dfs(G,v); } } /** * 該算法的關鍵步驟在於onStack數組的運用. * onStack數組標記的是當前遍歷的點.如果對於一個點指向的所有點中的某個點 * onstack[v]=true.代表該點正在被遍歷也就是說 * 該點存在一條路徑,指向這個點.而這個點現在又可以指向該點, * 即存在環的結構~ * @param G * @param 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); } 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); } } //dfs方法結束,對於該點的遞歸調用結束.該點指向的所有點已經遍歷完畢 onStack[v]=false; } private boolean hasCycle() { return cycle!=null; } public Iterable<Integer> cycle() { return cycle; } }
五.頂點的深度優先次序以及拓補排序
拓補排序:給定一幅有向圖,將所有的頂點排序,使得所有的有向邊均從排在前面的元素指向排在后面的元素.如果存在有向環的話,那么拓補排序無法完成.
要實現有向圖的拓補排序,利用標准深度優先搜索順序即可完成任務.這里頂點會有三種排列順序:
1.前序:在遞歸調用前將頂點加入隊列
2.后序:在遞歸調用之后將頂點加入隊列
3.逆后序:在遞歸調用之后將頂點壓入棧.
具體的操作見下面的代碼:
//有向圖中基於深度優先搜索的拓補排序 public class DepthFirstOrder { private boolean[] marked; private Queue<Integer> pre; //所有頂點的前序排列 private Queue<Integer> post; //所有頂點的后序排列 private Stack<Integer> reversePost;//所有頂點的逆后序排列 public DepthFirstOrder(Digraph G) { pre=new Queue<>(); post=new Queue<>(); reversePost=new Stack<>(); 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) { pre.enqueue(v); marked[v]=true; for(int w:G.adj(v)) { if(!marked[w]) { dfs(G,w); } } post.enqueue(v); reversePost.push(v); } public Iterable<Integer> pre() { return pre; } public Iterable<Integer> post() { return post; } public Iterable<Integer> reversePost() { return reversePost; } }
拓補排序的實現依賴於上面的API,實際上拓補排序即為所有頂點的逆后序排列,證明如下:
對於任意邊v->w,在調用dfs(v)的時候,下面三種情況必然有一種成立:
1.dfs(w)被調用且返回.(此時w被標記)
2.dfs(w)還沒有被調用,因此v->w會返回dfs(w),且dfs(w)在dfs(v)之前返回
3.dfs(w)已經被調用沒有返回,在有向無環圖中這種情況不可能!
在1,2這兩種情況中dfs(w)都在dfs(v)之前返回,也就是說在逆后序排列中,順序為v,w.滿足拓補排序的要求!
拓補排序的代碼如下:
public class Topological { private Iterable<Integer> order; //頂點的拓補排序 public Topological(Digraph G) { DirectedCycle cyclefinder=new DirectedCycle(G); if(!cyclefinder.hasCycle()) {//只有無環才能進行拓補排序 DepthFirstOrder dfs=new DepthFirstOrder(G); order=dfs.reversePost(); } } public Iterable<Integer> order() { return order; } public boolean isDAG() { return order!=null; } }
六.有向圖的強連通性
定義:如果兩個頂點v和w是互相可達的,則稱它們為強連通的.也就是說既存在一條從v到w的有向路徑也存在一條從w到v的有向路徑.如果一幅有向圖中的任意兩個頂點都是強連通的,則稱這副有向圖也是強連通的.任意頂點和自己都是強連通的.
強連通性將頂點分為了一些等價類,每個等價類都是由相互均為強連通的頂點的最大子集組成的.我們將這些子集稱為強連通分量.需要注意的是強連通分量是基於頂點的,而並非基於邊.
下面的代碼采用如下步驟來計算強連通分量以及兩個點是否是強連通的:
1.在給定的有向圖中,使用DepthFirsetOrder來計算它的反向圖GR的逆后序排列
2.按照第一步計算得到的順序采用深度優先搜索來訪問所有未被標記的點
3.在構造函數中,所有在同一個遞歸dfs()調用中被訪問到的頂點都是在同一個強連通分量中.
下面的代碼實現遵循了上面的思路:
/** * 該算法實現的關鍵: * 使用深度優先搜索查找給定有向圖的反向圖GR.根據由此得到的所有頂點的逆后序 * 再次用深度優先搜索處理有向圖G.其構造函數的每一次遞歸調用所標記的頂點都在 * 同一個強連通分量中. * 解決問題: * 判斷兩個點是否是強連通的 * 判斷總共有多少個連通分量 * @author Administrator * */ 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; } }
為了驗證這個代碼的正確性,需要證明這個命題,即:
使用深度優先搜索查找給定有向圖的GR,根據由此得到的所有頂點的逆后序再次用深度優先搜索處理有向圖G,其構造函數的每一次遞歸調用所標記的頂點都在同一個強連通分量中.
證明如下:
首先用反證法證明每個和s強連通的頂點v都會在構造函數中調用的dfs(G,s)中被訪問到.假設有一個頂點v沒有被訪問到.因為存在從s到v的路徑,那么說明v肯定在之前被訪問過了.但是因為也存在v到s的路徑,那么dfs(G,v)調用中,s肯定會被標記,因此構造函數肯定不會調用dfs(G,s)的,矛盾.
其次要證明構造函數調用的dfs(G,s)所到達的任意頂點v都必然是和s強連通的.設v為dfs(G,s)所到達的某個頂點.那么G中必然存在從s到v的路徑.因此現在只需要證明在GR中存在一條從s到v的路徑即可.而由於按照逆后序進行的深度優先搜索,因此在GR中進行的深度優先搜索意味着,dfs(G,v)必然在dfs(G,s)之前就結束了.這樣dfs(G,v)的調用存在兩種情況:
1.調用在dfs(G,s)的調用之前(並且也在dfs(G,s)的調用之前結束.這種情況不可能存在,因為在GR中存在一條從v到s的路徑.
2.調用在dfs(G,s)的調用之后(並且也在dfs(G,s)的結束之前結束.這種情況說明GR中存在一條從s到v的路徑.證明完成.
