有向圖的基本演算法-Java實現
有向圖
有向圖同無向圖的區別為每條邊帶有方向,表明從一個頂點至另一個頂點可達。有向圖的演算法多依賴深度搜索演算法。
本文主要介紹有向圖的基本演算法,涉及圖的表示、可達性、檢測環、圖的遍歷、拓撲排序以及強連通檢測等演算法。
1 定義有向圖
採用鄰接表結構儲存邊資訊,同時提供reverse介面生成反向圖,倒置每個邊的方向,該介面在後續其他演算法中會用到。
/** * 採用鄰接表表示的有向圖 */ 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 有向圖的可達性
有向圖的可達性是指給定一個或一組頂點,判斷是否可以到達圖中其他頂點。垃圾清除常見演算法“標記-清除”演算法中,採用有向圖的可達性演算法
標記所有可以被訪問的物件,然後在回收階段,僅僅回收那些未被標記的物件。
/** * 基於深度優先的有向圖可達性演算法 * 求出給定頂點或一組頂點,有向圖中能到達的點 */ 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 單點有向路徑和單點最短有向路徑
分別採用深度優先搜尋和廣度優先搜尋實現
有向圖的路徑
/** * 單點有向路徑,給定頂點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(); } }
有向圖的最短路徑,基於廣度優先演算法
/**
* 基於廣度優先搜尋的單向路徑演算法;
* 在此方法下,求得的路徑為最短路徑(忽略邊權重)
*/
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