有向圖的基本演算法-Java實現

形形色色?發表於2020-09-21

有向圖
有向圖同無向圖的區別為每條邊帶有方向,表明從一個頂點至另一個頂點可達。有向圖的演算法多依賴深度搜尋演算法。
本文主要介紹有向圖的基本演算法,涉及圖的表示、可達性、檢測環、圖的遍歷、拓撲排序以及強連通檢測等演算法。

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

相關文章