1.有向圖的數據類型
使用Bag表示有向圖,其中邊v->w表示為頂點v所對應的鄰接鏈表中包含一個w頂點,與無向圖不同的是,這里每條邊只會出現一次.有向圖的數據結構類型如下:
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; } }
2.有向圖中的可達性
無向圖的連通性相似,同利用深度優先搜索可以解決有向圖中
單點可達性問題:即:給定一幅有向圖和一個起點s,回答是否存在一條從s到達給定頂點v的有向路徑的問題.
多點可達性問題:給定一幅有向圖和頂點的集合,回答是否存在一條從集合中的任意頂點到達給定頂點v的有向路徑?
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]; } }
多點可達性問題的一個重要時機應用是在典型的內存管理系統中,包括許多java的實現。在一個有向圖中,一個頂點表示一個對象,一條邊則表示一個對象對另一個對象的引用。
這個模型很好表現了運行中的java程序的內存使用狀況。在程序執行的任何時候都有某些對象是可以被直接訪問的,而不能通過這些對象訪問到的所有對象都應該被回收以便
釋放內存。它會周期性的運行一個類似於DirectedDFS的有向圖可達性算法來標記所有可以被訪問到的對象。
3.有向圖的尋路
和無向圖類似,有向圖中常見的問題:
單點有向路徑。給定一幅有向圖和一個起點,回答“從s到給定目的頂點v是否存在一條有向路徑?如果有,找出這條路徑”
單點最短有向路徑。給定一幅有向圖和一個起點,回答“從s到給定目的頂點v是否存在一條有向路徑,如果有,找出其中最短的那條(所含邊數最少)”
4.調度問題—拓撲排序
4.1尋找有向環
如果一個有優先限制的問題中存在有向環,那么這個問題肯定是無解的。所以需要進行有向環的檢測。
下面的代碼可以用來檢測給定的有向圖中是否含有有向環,如果有,則按照路徑的方向返回環上的所有頂點.
在執行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; } }
4.2 拓撲排序
拓補排序:給定一幅有向圖,將所有的頂點排序,使得所有的有向邊均從排在前面的元素指向排在后面的元素.如果存在有向環的話,那么拓補排序無法完成.
要實現有向圖的拓補排序,利用標准深度優先搜索順序即可完成任務.這里頂點會有三種排列順序:
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; } }
遍歷的順序取決於這個數據結構的性質以及是在遞歸調用之前還是之后進行保存。
前序:在遞歸調用之前將頂點加入隊列。
后序:在遞歸調用之后將頂點加入隊列。
逆后序:在遞歸調用之后將頂點壓入棧。
前序就時dfs()的調用順序;后序就是頂點遍歷完成的順序;逆后序就是頂點遍歷完成順序的逆。
拓補排序的實現依賴於上面的API,實際上拓補排序即為所有頂點的逆后序排列
拓補排序的代碼如下:
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; } }
5.有向圖的強連通性
定義:如果兩個頂點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; } }