有向圖
有向圖同無向圖的區別爲每條邊帶有方向,代表從一個頂點至另外一個頂點可達。有向圖的算法多依賴深度搜索算法。
本文主要介紹有向圖的基本算法,涉及圖的表示、可達性、檢測環、圖的遍歷、拓撲排序以及強連通檢測等算法。java
1 定義有向圖算法
採用鄰接表結構存儲邊信息,同時提供reverse接口生成反向圖,倒置每一個邊的方向,該接口在後續其餘算法中會用到。post
/** * 採用鄰接表表示的有向圖 */ public class DiGraph { private final int V; private int E; private ArrayList<Integer>[] adj; public DiGraph(int V) { this.V = V; E = 0; adj = new ArrayList[V]; for (int i = 0; i < V; i++) { adj[i] = new ArrayList<>(); } } public DiGraph(Scanner scanner) { this(scanner.nextInt()); int E = scanner.nextInt(); for (int i = 0; i < E; i++) { int v = scanner.nextInt(); int w = scanner.nextInt(); addEdge(v, w); } } public void addEdge(int v, int w) { // 添加一條v指向w的邊 adj[v].add(w); E++; } /** * 返回有向圖的反向圖, 將每條邊的方向反轉 */ public DiGraph reverse() { DiGraph diGraph = new DiGraph(V); for (int v = 0; v < V; v++) { for (int w : adj[v]) { diGraph.addEdge(w, v); } } return diGraph; } public void show() { System.out.println("V: " + V); System.out.println("E: " + E); for (int i = 0; i < V; i++) { System.out.print(i + ": "); for (Integer integer : adj[i]) { System.out.print(integer + " "); } System.out.println(); } } public static void main(String[] args) { Scanner scanner = new Scanner(System.in); // 輸入用例參加附錄1 DiGraph diGraph = new DiGraph(scanner); // 輸入結果見附錄2 diGraph.show(); } }
2 有向圖的可達性測試
有向圖的可達性是指給定一個或一組頂點,判斷是否能夠到達圖中其餘頂點。垃圾清除常見算法「標記-清除」算法中,採用有向圖的可達性算法
標記全部能夠被訪問的對象,而後在回收階段,僅僅回收那些未被標記的對象。this
/** * 基於深度優先的有向圖可達性算法 * 求出給定頂點或一組頂點,有向圖中能到達的點 */ public class DirectedDFS { private boolean[] marked; // 標記每一個頂點是否可到達 public DirectedDFS(DiGraph G, int s) { marked = new boolean[G.V()]; dfs(G, s); } public DirectedDFS(DiGraph G, Iterable<Integer> sources) { marked = new boolean[G.V()]; for (int v : sources) { if(!marked[v]){ dfs(G, v); } } } private void dfs(DiGraph G, int v) { marked[v] = true; for (int w : G.adj(v)) { if(!marked[w]) dfs(G, w); } } public boolean marked(int v) { return marked[v]; } public static void main(String[] args) { // 輸入用例參加附錄1 DiGraph diGraph = new DiGraph(new Scanner(System.in)); // 輸出結果參加附錄3 // 測試頂點2到達的點 System.out.println("頂點2到達的點"); DirectedDFS reachable = new DirectedDFS(diGraph, 2); for (int i = 0; i < diGraph.V(); i++) if(reachable.marked(i)) System.out.print(i + " "); System.out.println(); // 測試一組點:1,2,6可以到達的點 System.out.println("1,2,6可以到達的點"); DirectedDFS reachable2 = new DirectedDFS(diGraph, Arrays.asList(1, 2, 6)); for (int i = 0; i < diGraph.V(); i++) if(reachable2.marked(i)) System.out.print(i + " "); System.out.println(); } }
3 單點有向路徑和單點最短有向路徑code
分別採用深度優先搜索和廣度優先搜索實現
有向圖的路徑對象
/** * 單點有向路徑,給定頂點v,肯定對於圖中任一點w; * 是否存在v到w的路徑,並輸出路徑; * 注意,深度優先搜索的路徑沒法保證是最短路徑 */ public class DigraghDepthFirstPaths { // 標記點是否可達 private boolean[] marked; // 記錄到達點的那條邊 private int[] edge; private final int s; public DigraghDepthFirstPaths(DiGraph G, int s) { this.s = s; marked = new boolean[G.V()]; edge = new int[G.V()]; edge[s] = s; dfs(G, s); } private void dfs(DiGraph G, int v) { marked[v] = true; for (int w : G.adj(v)) { if(!marked[w]){ edge[w] = v; dfs(G, w); } } } public boolean hasPathTo(int v){ return marked[v]; } public Stack<Integer> pathTo(int v) { Stack<Integer> paths = new Stack<>(); for (int x=v; x!=s; x=edge[x]){ paths.add(x); } paths.add(s); return paths; } public static void main(String[] args) { // 輸入用例參加附錄1 DiGraph diGraph = new DiGraph(new Scanner(System.in)); // 輸出結果參加附錄4 // 構建頂點0到其餘頂點的有向路徑 DigraghDepthFirstPaths depthFirstPaths = new DigraghDepthFirstPaths(diGraph, 0); System.out.print("頂點0可達的點: "); for (int i = 0; i < diGraph.V(); i++) { if (depthFirstPaths.hasPathTo(i)) System.out.print(i + " "); } System.out.println(); // 是否存在有向路徑 if(depthFirstPaths.hasPathTo(12)) System.out.println("0至12存在有向路徑"); else System.out.println("0至12不存在有向路徑"); // 頂點0到頂點3的一條有向路徑 System.out.print("0至3的一條有向路徑: "); Stack<Integer> pathTo = depthFirstPaths.pathTo(3); while (!pathTo.isEmpty()){ if (pathTo.size() == 1) System.out.print(pathTo.pop()); else System.out.print(pathTo.pop() + " -> "); } System.out.println(); } }
有向圖的最短路徑,基於廣度優先算法blog
/** * 基於廣度優先搜索的單向路徑算法; * 在此方法下,求得的路徑爲最短路徑(忽略邊權重) */ public class DigraphBreadthFirstPaths { private boolean[] marked; // 採用隊列保持帶訪問的頂點 private ArrayDeque<Integer> enqueue; private int[] edge; private final int s; public DigraphBreadthFirstPaths(DiGraph G, int s) { this.s = s; marked = new boolean[G.V()]; edge = new int[G.V()]; enqueue = new ArrayDeque<>(); enqueue.add(s); bfs(G); } private void bfs(DiGraph G) { while (!enqueue.isEmpty()) { int v = enqueue.poll(); for (int w : G.adj(v)) { if(!marked[w]){ edge[w] = v; marked[w] = true; enqueue.add(w); } } } } public boolean hasPathTo(int v){ return marked[v]; } public Stack<Integer> pathTo(int v) { Stack<Integer> paths = new Stack<>(); for (int x=v; x!=s; x=edge[x]){ paths.add(x); } paths.add(s); return paths; } public static void main(String[] args) { // 輸入用例參加附錄1 DiGraph diGraph = new DiGraph(new Scanner(System.in)); // 輸出結果參加附錄5 // 構建頂點0到其餘頂點的有向路徑 DigraphBreadthFirstPaths breadthFirstPaths = new DigraphBreadthFirstPaths(diGraph, 0); System.out.print("頂點0可達的點: "); for (int i = 0; i < diGraph.V(); i++) { if (breadthFirstPaths.hasPathTo(i)) System.out.print(i + " "); } System.out.println(); // 是否存在有向路徑 if(breadthFirstPaths.hasPathTo(12)) System.out.println("0至12存在有向路徑"); else System.out.println("0至12不存在有向路徑"); // 頂點0到頂點3的最短路徑 System.out.print("0至3的一條有向路徑: "); Stack<Integer> pathTo = breadthFirstPaths.pathTo(3); while (!pathTo.isEmpty()){ if (pathTo.size() == 1) System.out.print(pathTo.pop()); else System.out.print(pathTo.pop() + " -> "); } System.out.println(); } }
4 檢測有向圖的環排序
檢測有向圖是否包含環,檢測圖沒有環是拓撲排序的前提條件。
多數狀況下,須要知道有向圖是否包含環,而且輸出夠成環的邊。遞歸
/** * 基於深度優先搜索檢測圖中是否包含環 */ public class DirectedCycle { private boolean[] onStack; private Stack<Integer> cycle; private int[] edge; private boolean[] marked; public DirectedCycle(DiGraph G) { onStack = new boolean[G.V()]; edge = new int[G.V()]; marked = new boolean[G.V()]; for (int i = 0; i < G.V(); i++) { if(!marked[i]) dfs(G, i); } } 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]){ edge[w] = v; dfs(G, w); } // onStack[w]爲true代表,當前v節點是一條通過w的抵達,代表w -> v有路徑 // 因爲v -> w有邊,所以必爲環 else if(onStack[w]){ cycle = new Stack<>(); for (int x = v; x != w; x=edge[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; } public static void main(String[] args) { // 輸入用例參加附錄1 DiGraph diGraph = new DiGraph(new Scanner(System.in)); // 輸出結果參加附錄6 DirectedCycle directedCycle = new DirectedCycle(diGraph); System.out.println("有向圖是否包含環: " + (directedCycle.hasCycle() ? "是" : "否")); if (directedCycle.hasCycle()){ System.out.print("其中一條環爲:"); for (int i : directedCycle.cycle()) { System.out.print(i + " "); } } System.out.println(); } }
5 頂點的深度優先次序
頂點的深度優先次序分爲前序、後序和逆後續,區別是記錄點的時機發生在遞歸調用的前仍是後。該算法產生的pre、post和reversePost
順序在圖的高級算法中十分有用。
public class DepthFirstOrder { private boolean[] marked; private ArrayDeque<Integer> pre; // 保存前序遍歷的結果 private ArrayDeque<Integer> post; // 保存後序的遍歷結果 private ArrayDeque<Integer> reversePost; //保存逆後序的遍歷結果 public DepthFirstOrder(DiGraph G) { marked = new boolean[G.V()]; pre = new ArrayDeque<>(); post = new ArrayDeque<>(); reversePost = new ArrayDeque<>(); for (int v=0; v<G.V(); v++) if (!marked[v]) dfs(G, v); } private void dfs(DiGraph G, int v) { marked[v] = true; pre.add(v); for (int w : G.adj(v)) if(!marked[w]) dfs(G, w); post.add(v); // 按post的倒序保存 reversePost.addFirst(v); } public Iterable<Integer> pre(){ return pre; } public Iterable<Integer> post(){ return post; } public Iterable<Integer> reversePost(){ return reversePost; } public static void main(String[] args) { // 構造無環圖的輸入參見附錄7 DiGraph diGraph = new DiGraph(new Scanner(System.in)); DepthFirstOrder depthFirstOrder = new DepthFirstOrder(diGraph); // 輸出結果參加附錄8 // 注意:對於同一幅圖,構造圖的輸入順序不一致 // 會致使輸出不相同 System.out.print("前序節點順序: "); for (int v : depthFirstOrder.pre()) System.out.print(v + " "); System.out.println(); System.out.print("後續節點順序:"); for (int v : depthFirstOrder.post()) System.out.print(v + " "); System.out.println(); System.out.print("逆後序節點順序:"); for (int v : depthFirstOrder.reversePost()) System.out.print(v + " "); } }
6 拓撲排序
給定一幅有向圖,給出一組頂點排序,在有向圖中,全部的邊均是前面的點指向後面的點。
拓撲排序依賴圖的環檢測和逆後序遍歷算法。
/** * 計算有向無環圖中的全部頂點的拓撲排序, * 一般用於解決優先級限制下的調度問題 */ public class Topological { private Iterable<Integer> order; public Topological(DiGraph G) { DirectedCycle directedCycle = new DirectedCycle(G); if(!directedCycle.hasCycle()) order = new DepthFirstOrder(G).reversePost(); } public boolean isDAG(){ return order == null; } public Iterable<Integer> order(){ return order; } public static void main(String[] args) { // 輸入用例參考附錄7 DiGraph diGraph = new DiGraph(new Scanner(System.in)); Topological topological = new Topological(diGraph); // 輸出結果參見附錄9 if (topological.isDAG()) System.out.println("有向圖帶有環,沒法進行拓撲排序"); else{ System.out.print("拓撲排序結果:"); for (int v : topological.order()) { System.out.print(v + " "); } } } }
7 強聯通檢測
若是存在從v至w的路徑,同時還存在從w至v的路徑,則稱v和w之間是強連通;若是一幅有向圖中任意兩點間都
是強連通,則這幅有向圖也是強連通的。檢測強連通算法依賴圖的反轉和逆後序遍歷算法。算法比較簡潔,可是
理解起來比較難,須要仔細分析理解。
/** * 有向圖的強連通性,該算法依賴逆後序排序、圖的反轉、無向圖的聯通性算法 */ public class SCC { private int[] id; private int count; private boolean[] marked; public SCC(DiGraph G) { id = new int[G.V()]; marked = new boolean[G.V()]; DepthFirstOrder depthFirstOrder = new DepthFirstOrder(G.reverse()); for (int v : depthFirstOrder.reversePost()) if(!marked[v]) { dfs(G, v); count++; } } private void dfs(DiGraph G, int v) { id[v] = count; marked[v] = true; 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 count(){ return count; } // 節點所在的聯通份量標識符 public int id(int v){ return id[v]; } public static void main(String[] args) { // 帶環的圖,輸入用例參見附錄1 DiGraph diGraph = new DiGraph(new Scanner(System.in)); // 輸出結果參見附錄10 SCC scc = new SCC(diGraph); System.out.println("有向圖中強連通份量數:" + scc.count()); System.out.println("節點6與12是不是強連通:" + (scc.stronglyConnected(6, 12) ? "是" : "否")); System.out.println("節點9與12是不是強連通:" + (scc.stronglyConnected(9, 12) ? "是" : "否")); System.out.println("輸出聯通份量"); for (int i = 0; i < scc.count(); i++) { for (int v = 0; v < diGraph.V(); v++) { if(scc.id[v] == i) System.out.print(v + " "); } System.out.println(); } } }
附錄1,有向圖構造數據
13 22 4 2 2 3 3 2 6 0 0 1 2 0 11 12 12 9 9 10 9 11 8 9 10 12 11 4 4 3 3 5 7 8 8 7 5 4 0 5 6 4 6 9 7 6
附錄2,有向圖輸出
V: 13 E: 22 0: 1 5 1: 2: 3 0 3: 2 5 4: 2 3 5: 4 6: 0 4 9 7: 8 6 8: 9 7 9: 10 11 10: 12 11: 12 4 12: 9
附錄3:有向圖的可達性測試
頂點2到達的點 0 1 2 3 4 5 1,2,6可以到達的點 0 1 2 3 4 5 6 9 10 11 12
附錄4:基於深度優先搜索的單向路徑測試結果
頂點0可達的點: 0 1 2 3 4 5 0至12不存在有向路徑 0至3的一條有向路徑: 0 -> 5 -> 4 -> 2 -> 3
附錄5:基於廣度優先搜索的最短路徑測試結果
頂點0可達的點: 0 1 2 3 4 5 0至12不存在有向路徑 0至3的一條有向路徑: 0 -> 5 -> 4 -> 3
附錄6:檢測環算法的測試輸出
有向圖是否包含環: 是 其中一條環爲:3 2 4 5 3
附錄7:構造無環圖的輸入用例
13 15 0 1 0 5 0 6 2 0 2 3 3 5 5 4 6 4 6 9 7 6 8 7 9 10 9 11 9 12 11 12
附錄8:深度優先遍歷圖的輸出結果
前序節點順序: 0 1 5 4 6 9 10 11 12 2 3 7 8 後續節點順序:1 4 5 10 12 11 9 6 0 3 2 7 8 逆後序節點順序:8 7 2 3 0 6 9 11 12 10 5 4 1
附錄9:拓撲排序測試輸出結果
拓撲排序結果:8 7 2 3 0 6 9 11 12 10 5 4 1
附錄10:帶環有向圖的強連通性測試輸出結果
有向圖中強連通份量數:5 節點6與12是不是強連通:否 節點9與12是不是強連通:是 輸出聯通份量 1 0 2 3 4 5 9 10 11 12 6 7 8