數據結構&算法拾遺(5)-- 有向無環圖(DAG)與拓撲排序(調度)

在有向圖中,邊是單向的;每條邊所連接的兩個頂點都是一個有序對,它們的鄰接性是單向的。有向圖的應用方向很多,如網絡、任務調度條件或者是電話關係等都是天然的有向圖。今天主要介紹有向無環圖(DAG)與拓撲排序之間的關係。

1. 術語

拓撲排序:對一個有向無環圖(Directed Acyclic Graph簡稱DAG)G進行拓撲排序,是將G中所有頂點排成一個線性序列,使得圖中任意一對頂點u和v,若邊<u,v>∈E(G),則u在線性序列中出現在v之前。通常,這樣的線性序列稱爲滿足拓撲次序(Topological Order)的序列,簡稱拓撲序列。簡單的說,由某個集合上的一個偏序得到該集合上的一個全序,這個操作稱之爲拓撲排序

簡單的說,將調度任務想象爲DAG圖,每個頂點之間有先後次序,如何使用一個線性結構來執行這些調度任務,並且不違背它們之間的先後次序,解決這個問題,就可以使用有向無環圖的拓撲排序算法。

2.有向圖的數據類型

首先老規矩,先介紹有向圖的數據類型及其代碼實現,有向圖的API如下圖所示:


有向圖的api
返回值類型 Digraph類方法 說明
  Digraph(int V) 創建一幅含有V個頂點但沒有邊的有向圖
  Digraph(In in) 從輸入流in 中讀取一幅有向圖
int V() 頂點總數
int E() 邊的總數
void addEdge(int v, int w) 向有向圖中添加一條邊v-->w
Iterable<Integer> adj(int v) 由 v指出的邊所連接的所有頂點
Digraph reverse() 該圖的反向圖
String toString() 對象的字符串表示

實現代碼如下:

package algorithms.digraph;

import edu.princeton.cs.algs4.Bag;
import edu.princeton.cs.algs4.In;


/**
 * Created by xudong on 2019/8/16.
 */
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 Digraph(In in){
        this(in.readInt());     //讀取V並將圖初始化
        int E = in.readInt();   //讀取E
        for( int i =0; i < E; i++){
            //添加一條邊
            int v = in.readInt(); //讀取一個頂點
            int w = in.readInt(); //讀取另一個頂點
            addEdge(v, w); // 添加一條連接他們的邊
        }
    }

    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 v = 0; v < V; v++ ){
            for(int w : adj(v))
                R.addEdge(w, v);
        }
        return R;
    }

    //圖的鄰接表的字符串表示
    public String toString(){
        StringBuffer s = new StringBuffer();
        s.append(V + "vertices, " + E + " edges\n");
        for( int v = 0; v < V; v++){
            s.append(v + ": ");
            for( int w : this.adj(v) ){
                s.append(w + " ");
            }
            s.append("\n");
        }
        return s.toString();
    }



}

 輸入格式如下圖所示:

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

3. 有向圖解決的問題

3.1 可達性問題

對於有向圖中的可達性問題(“是否存在一條從S到達給定頂點V的有向圖”  或者  “是否存在一條從集合中的任意頂點到達給定頂點V的有向路徑”)可以使用深度優先搜索來解決這些問題(它對每個頂點調用遞歸方法dfs(), 以標記遇到的任意頂點)代碼如下所示:

package algorithms.digraph;

import edu.princeton.cs.algs4.Bag;
import edu.princeton.cs.algs4.In;
import edu.princeton.cs.algs4.StdOut;

/**
 * 有向圖的可達性API
 * 多點可達性的一個重要應用是 典型的內存管理系統中,在java的內存實現中,一個頂點表示一個對象,一條邊表示一個對象對另一
 * 個的引用,標記-清除 的垃圾回收策略會爲每一個對象保留一個標誌位作垃圾回收使用,週期性的運行一個類似DirectedDFS的有向圖
 * 可達性算法來標記所有可以被訪問到的對象,然後清理所有未被標記的對象
 * DepthFirstPaths 和 BreadthFirstPaths 也是有向圖的重要算法,他們主要解決以下問題:
 * 單點有向路徑:“從s到給定目的定點v是否有一條有向路徑”
 * 單點最短有向路徑: “從s到給定目的頂點是否有一條有向路徑, 如果有,找出最短路徑”
 * Created by xudong on 2019/8/17.
 */
public class DirectedDFS {
    private boolean[] marked;

    // 在 有向圖 G 中, 端點s 開始 的可達路徑有哪些
    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 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);
        }
    }

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

    public static void main(String[] args) {
        Digraph G = new Digraph(new In("tinyDG.txt"));
        Bag<Integer> sources = new Bag<Integer>();
        for(int i = 0; i < args.length; i++){
            sources.add(Integer.parseInt(args[i]));
        }

        DirectedDFS reachable = new DirectedDFS(G, sources);
        for(int v = 0; v < G.V(); v++){
            if(reachable.marked(v))
                StdOut.print(v + " ");
        }
        System.out.println();
    }

}

3.2 環、有向無環圖與調度問題

有向環:在與有向圖相關的應用中,有向環特別的重要,在實際應用中,我們可能會關注其是否在圖中存在。

調度問題:一種應用廣泛的模型是指定一組任務並且安排它們的執行順序,限制條件是這些任務的執行方法和起始時間,可能還包括任務的時耗或其他資源,其中最重要的一種限制條件叫做優先級限制,它指明瞭哪些任務必須在哪些任務之前完成。對於這類問題,我們都可以立馬畫一張有向圖,其中頂點對應任務,有向邊對應優先級順序,簡明起見優先級限制下的調度問題等價於下面這個基本問題(拓撲排序):

拓撲排序:給定一幅有向圖,將所有的頂點排序,使得所有的有向邊均從排在前面的元素指向排在後面的元素。在解決這個問題前,首先要確定圖是無環的。

檢測有向圖是否有環的代碼如下:

package algorithms.digraph;

import edu.princeton.cs.algs4.Stack;

/**
 * Created by xudong on 2019/8/17.
 * 檢測有向圖中是否有 環的存在, 保證該圖 爲有向無環圖(DAG), 因爲環的存在在有向圖中常常指代死鎖問題,
 * 所以建模需要有向無環圖
 */
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<Integer>();
                for(int x = v; x != w; x = edgeTo[x]){
                    cycle.push(x);
                }
                cycle.push(w);
                cycle.push(v);   // w的上一個是 v
            }
        }

        onStack[v] = false;   //深度優先結束後將 v 出棧
    }

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

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

}

以上代碼基於這樣一個事實:在深度優先搜索中,由系統維護的遞歸調用的棧表示的正式當前正在遍歷的有向路徑。一旦我們找到了一條有向邊v-->w且w已經存在於棧中,就找到了一個環,因爲棧表示的是一條由w到v的有向路徑,而v-->w正好補全了這個環。

 

頂點的深度優先次序排序與拓撲排序:

優先級限制下的調度問題等價於計算有向無環圖中的所有頂點的拓撲順序。

package algorithms.digraph;


import edu.princeton.cs.algs4.Graph;
import edu.princeton.cs.algs4.Queue;
import edu.princeton.cs.algs4.Stack;

/**
 *  計算有向圖中頂點的深度優先次序(前序、後序和逆後序)
 *  前序: 在遞歸調用之前將頂點加入隊列
 *  後序:在遞歸調用之後將頂點加入隊列
 *  逆後序:在遞歸調用之後將頂點加入棧
 */
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<Integer>();
        post = new Queue<Integer>();
        reversePost = new Stack<Integer>();
        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[v]){
                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;
    }
}

 

 

package algorithms.digraph;

import edu.princeton.cs.algs4.SymbolDigraph;

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;
    }

    //G是有向無環圖嗎
    public boolean isDAG(){
        return order != null;
    }

    public static void main(String[] args) {

    }
}

 

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