有向圖:學習總結

有向圖

相較於無向圖,有向圖的邊是帶有方向性的。v→w,那麼v的鄰接點鏈表中會有w,但是w的鄰接點鏈表不存在v。因此就鄰接表來說,體現有向性是通過鏈表中的節點有無來實現的。

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 i = 0; i < V; i++) {
            adj[V] = new Bag<>();
        }
    }

    public int V() {
        return V;
    }

    public int E() {
        return E;
    }

    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 i = 0; i < V; i++) {
            for (int w : adj(i)) {
                // 通過相反的方式添加邊
                R.addEdge(w, i);
            }
        }
        return R;
    }

}

有向圖的可達性

public class DirectedDFS {
    private boolean[] marked;

    public DirectedDFS(Digraph G, int s) {
        marked = new boolean[G.V()];
        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);
        }
    }

    public boolean marked(int v){
        return marked[v];
    }
}

拓撲排序

定義:給定一幅有向圖,將所有的頂點排序,使得所有的有向邊均從排在前面的元素指向排在後面的元素

優先級調度問題中,環的存在時不允許的,這會打破優先級熟悉怒,因此環的檢測十分重要。

環檢測算法及其對應的數據結構:

public class DirectedCycle {

    private boolean[] marked;
    private int[] edgeTo;
    private Stack<Integer> cycle; // 如果有環,創建這個環
    private boolean[] onStack; // 保存遞歸調用時,棧上的所有頂點

    public DirectedCycle(Digraph G) {
        onStack = new boolean[G.V()];
        edgeTo = new int[G.V()];
        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) {
        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<>();
                // 構建這個環,依次獲取DFS中頂點的上級頂點
                for (int x = v; x != w; x = edgeTo[x]) {
                    cycle.push(x);
                }
                cycle.push(w);
                cycle.push(v);
            }
        }
        onStack[v] = false;
    }

    private boolean hasCycle() {
        return cycle != null;
    }

    public Iterable<Integer> cycle() {
        return cycle;
    }
}

拓撲排序要點

  • 在進行DFS時,將參數頂點保存在一個數據結構中,遍歷這個數據結構實際上就能訪問圖中的所有頂點。
  • 排序順序(注意數據結構的使用):
    • 前序:在遞歸調用之前將頂點加入隊列
    • 後序:在遞歸調用之後將頂點加入隊列
    • 逆後序:在遞歸調用之後將頂點壓入

三種排序方式的數據結構 – 可類比樹的遍歷

public class DepthFirstOrder {
    private boolean[] marked;
    private Queue<Integer> pre;
    private Queue<Integer> post;
    private Stack<Integer> reversePost;

    public DepthFirstOrder(Digraph G) {
        // 用原生JDK實現
        pre = new LinkedList<>();
        post = new LinkedList<>();
        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.offer(v);
        
        // DFS 過程
        marked[v] = true;
        for (int w : G.adj(v)) 
            if (!marked[w]) dfs(G, w);
        
        // 逆序
        post.offer(v);
        // 逆後序
        reversePost.push(v);
    }

    public Queue<Integer> pre() {
        return pre;
    }

    public Queue<Integer> post() {
        return post;
    }

    public Stack<Integer> reversePost() {
        return reversePost;
    }
}

拓撲算法實現:

public class Topological {

    // 頂點的拓撲排序
    private  Iterable<Integer> order;

    public Topological(Digraph G){
        // 檢測有無環
        DirectedCycle cycleDetector = new DirectedCycle(G);
        if(!cycleDetector.hasCycle()){
            // 無環則返回節點序列
            DepthFirstOrder dfs = new DepthFirstOrder(G);
            // 拓撲順序爲逆後序 -- 入度小的在前面
            order = dfs.reversePost();
        }
    }

    public Iterable<Integer> order() {
        return order;
    }
    public boolean isDAG(){
        return order != null;
    }

}

有向圖的強連通性

  • 定義:有向圖的兩個頂點v和w,如果是相互可達的,那麼稱他們強連通

  • 推論:頂點強連通 當且僅當 它們都在一個普通的有向環

  • 滿足離散數學推論中的:自反性、對稱性、傳遞性

  • 定義是基於點而不是邊

強連通分量的計算算法 – Kosaraju算法:

這個算法的理解比較噁心,首先需要理解這個算法的目的:

  • 保證在遍歷強連通分量時,有一個其他連通分量的頂點,在這個強連通分量的前面
  • 這樣檢驗完一個強連通分量後,可以進入下一個強連通分量
  • 強連通分量之間是可達,但不構成迴路的
  • 綜上所述,要進行統計之前,首先要構建一個序列,將各個連通分量隔離開(內部順序不定)

接下來是證明通過求反向圖的逆後序,可以保證兩個連通分量之間是隔離開的

可以看到,通過構建反向圖的逆後序排列,成功將各個強連通分量隔離開,並保證可以從一個強連通分量進入下一個強連通分量

image-20200616172859240

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()];

        // 核心部分 -- 其實完成了兩次DFS,在創建order的時候還進行了依次dfs
        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){
        // 如果所屬的連通分量id相同,則證明v和w相互連通
        return id[v] == id[w];
    }

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章