《算法》筆記 10 - 無向圖

  • 表示無向圖的數據結構
    • 鄰接表數組
  • 深度優先搜索
    • 深度優先搜索尋找路徑
    • 深度優先搜索的性能特點
  • 廣度優先搜索
  • 兩種搜索方式的對比

圖表示由相連的結點所表示的抽象模型,這個模型可以用來研究類似“能否從某個點到達指定的另一個點”、“有多少個結點和指定的結點相連”、“兩個結點之間最短的連接是哪一條”。圖的算法與很多實際問題相關。比如地圖、搜索引擎、電路、任務調度、商業交易、計算機網絡、社交網絡等。
無向圖是一種最簡單、最基本的圖模型,僅僅由一組頂點和一組能夠將兩個頂點相連的邊組成。
在圖的實現中,用從0開始的整數值來表示圖的結點,用類似8-5來表示連接結點8和5的邊,在無向圖中,這與5-8表示的是同一條邊。4-6-3-9表示的是4到9之間的一條路徑。

表示無向圖的數據結構

無向圖的API

 public class Graph{
    Graph(int V)   //創建一個含有V個頂點但不含有邊的圖
    Graph(In in)   //從標準輸入流in讀入一幅圖
    int v()     //頂點數
    int E()    //邊數
    void addEdge(int v, int w)    //向圖中添加一條邊v-w
    Iterable<Integer>adj(intv)     //和相鄰的所有頂點
    String toString()      //對象的字符串表示     
 }

在這裏插入圖片描述
第二個構造函數接受的輸入由2*E+2個整數組成,前兩行分別是V和E,表示圖中頂點和邊的數量。接下來每行都是一對互相連接的頂點。

鄰接表數組

可以選擇鄰接表數組作爲實現Graph的數據結構,它將每個頂點的所有相鄰頂點都保存在一張鏈表中,讀取tingG後構造的鄰接表數組如圖所示:
在這裏插入圖片描述

代碼實現:

public class Graph {
    private final int V; // vertex
    private int E; // edge
    private Bag<Integer>[] adj;

    public Graph(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 Graph(In in) {
        this(in.readInt());
        int E = in.readInt();
        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);
        adj[w].add(v);
        E++;
    }

    public Iterable<Integer> adj(int v) {
        return adj[v];
    }
}

用數組adj[]來表示圖的頂點,可以快速訪問給定頂點的鄰接頂點列表;用Bag數據類型來存儲一個頂點的所有鄰接頂點,可以保證在常數時間內添加新的邊或者遍歷任意頂點的鄰接頂點。要添加比如5-8這條邊時,addEdge方法除了會把8添加到5的鄰接表中,還會把5添加到8的鄰接表。

這種實現的性能特點爲:

  • 使用的空間和V+E成正比
  • 添加一條邊所需的時間爲常數
  • 遍歷頂點一個頂點的相鄰頂點所需的時間和這個頂點的度數成正比(頂點的度數表示與這個頂點相連的邊數)

深度優先搜索

深度優先搜索是一種遍歷圖的方式,這種算法的軌跡與走迷宮非常類似。可以將迷宮作爲圖,迷宮的通道作爲圖的邊,迷宮的路口作爲圖的點,迷宮可認爲是一種直觀的圖。探索迷宮的一種方法叫做Tremaux搜索。這種方法的具體做法是,選擇一條沒有標記過的通道,在走過的路上鋪一條繩子;標記所有第一次經過的路口和通道;當來到第一個標記過的路口時,回退到上一個路口;當回退的路口已沒有可走的通道時繼續回退。
這樣,最終可以找到一條出路,而且不會多次經過同一通道或者路口。
在這裏插入圖片描述

深度優先搜索的代碼實現與走迷宮類似:

public class DepthFirstSearch {
    private boolean[] marked;
    private int count;
    private final int s;

    public DepthFirstSearch(Graph G, int s) {
        marked = new boolean[G.V()];
        this.s = s;
        dfs(G, s);
    }

    private void dfs(Graph G, int v) {
        marked[v] = true;
        for (int w : G.adj(v)) {
            if (!marked[w]) {
                dfs(G, w);
            }
        }
    }

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

    public int count() {
        return count;
    }
}

這段代碼會搜索出所有與頂點s相鄰的點,中dfs()方法的遞歸調用機制以及marked數組對應迷宮中的繩子的作用,當已經處理完一個頂點的所有相鄰頂點後,遞歸會結束。算法在運行的時候,總是會沿着一個頂點的第一個相鄰頂點不斷深入,直到遇到一個在marked數組已經標記的頂點,才逐層退出遞歸,這也是深度優先搜索名稱的由來。最終搜索的結果存儲在marked數組中,標記爲true的位對應的索引就是與頂點s相連的點。

深度優先搜索尋找路徑

深度優先搜索可以解決路徑檢測問題,即回答“兩個給定的頂點之間是否存在一條路徑?”,但如果想找出這條路徑呢?要回答這個問題,只需要對上面的代碼稍作擴展:

public class DepthFirstPaths {
    private boolean[] marked;
    private int[] edgeTo;  //新增的,用於記錄路徑
    private final int s;

    public DepthFirstPaths(Graph G, int s) {
        marked = new boolean[G.V()];
        edgeTo = new int[G.V()];  
        this.s = s;
        dfs(G, s);
    }

    private void dfs(Graph G, int v) {
        marked[v] = true;
        for (int w : G.adj(v)) {
            if (!marked[w]) {
                edgeTo[w] = v;  //記錄路徑
                dfs(G, w);
            }
        }
    }

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

    public int count() {
        return count;
    }

    public boolean hasPathTo(int v) {   //判斷是否存在從s到v的路徑
        return marked(v);
    }

    public Iterable<Integer> pathTo(int v) {  //獲取從s到v的路徑,不存在則返回null
        if (!hasPathTo(v))
            return null;

        Stack<Integer> path = new Stack<Integer>();
        for (int x = v; x != s; x = edgeTo[x]) {
            path.push(x);
        }

        path.push(s);
        return path;
    }

    public static void main(String[] args) {
        In in = new In(args[0]);
        Graph G = new Graph(in);
        int s = Integer.parseInt(args[1]);
        DepthFirstPaths search = new DepthFirstPaths(G, s);

        //
        for (int v = 0; v < G.V(); v++) {
            StdOut.print(s+" to "+v+": ");
            if(search.hasPathTo(v)){
                for(int x:search.pathTo(v)){
                    if(x==s) StdOut.print(x);
                    else StdOut.print("-"+x);
                }
            }
            StdOut.println();
        }
    }
}

這段代碼添加了edgeTo[]整形數組來起到Tremaux搜索中繩子的作用。每次由邊v-w第一次訪問w時,會將edgeTo[w]設爲v,最終edgeTo數組是一顆以起點爲根節點的樹,記錄了由任意連通的結點回到根節點的路徑。
下圖爲由一副圖生成的edgeTo的內容,及路徑樹的結構的示例:
在這裏插入圖片描述

這與代碼運行結果是一致的:

java DepthFirstPaths tinyCG.txt 0
0 to 0:0
0 to 1:0-2-1
0 to 2:0-2
0 to 3:0-2-3
0 to 4:0-2-3-4
0 to 5:0-2-3-5

深度優先搜索的性能特點

深度優先搜索標記與起點連通的所有頂點所需的時間與頂點的度數之和成正比。
使用深度優先搜索得到從給定起點到任意標記頂點的路徑所需的時間與路徑的長度成正比。

廣度優先搜索

深度優先搜索得到的路徑不僅與圖的結構有關,還受圖的表示的影響,鄰接表中頂點的順序不同,得到的路徑也會不同。所以當需要計算兩點間的最短路徑(單點最短路徑)時,就無法依賴深度優先搜索了,而廣度優先搜索可以解決單點最短路徑問題。
要找到從s到v的最短路徑,從s開始,在所有由一條邊就可以到達的頂點中尋找v,如果找不到就繼續在於s距離兩條邊的頂點中查找,如此一直進行。

public class BreadthFirstPaths {
    private boolean[] marked;
    private int[] edgeTo;
    private final int s;

    public BreadthFirstPaths(Graph G, int s) {
        marked = new boolean[G.V()];
        edgeTo = new int[G.V()];
        this.s = s;
        bfs(G, s);
    }

    private void bfs(Graph G, int s) {
        Queue<Integer> queue = new Queue<Integer>();
        marked[s] = true;
        queue.enqueue(s);
        while (!queue.isEmpty()) {
            int v = queue.dequeue();
            for (int w : G.adj(v)) {
                if(!marked[w]){
                    edgeTo[w]=v;
                    marked[w]=true;
                    queue.enqueue(w);
                }
            }
        }
    }

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

    public Iterable<Integer> pathTo(int v) {
        if (!hasPathTo(v))
            return null;

        Stack<Integer> path = new Stack<Integer>();
        for (int a = v; a != s; a = edgeTo[a]) {
            path.push(a);
        }

        path.push(s);
        return path;
    }

    
     // cmd /c --% java algs4.four.BreadthFirstPaths ..\..\..\algs4-data\tinyCG.txt 0
     public static void main(String[] args) {
        In in = new In(args[0]);
        int s = Integer.parseInt(args[1]);
        Graph g = new Graph(in);
        BreadthFirstPaths search = new BreadthFirstPaths(g, s);

        for (int i = 0; i < g.V(); i++) {
            StdOut.print(i + ":");
            Iterable<Integer> path = search.pathTo(i);
            for (Integer p : path) {
                if (search.s != p) {
                    StdOut.print("-" + p);
                } else {
                    StdOut.print(p);
                }
            }
            StdOut.println();
        }
    }
}

方法bfs中定義了一個隊列來保存所有已經被標記過但其鄰接表還未被檢查過的頂點。先將起點加入隊列,然後重複以下步驟直到隊列爲空:

  • 取隊列中的下一個頂點v並標記它
  • 將與v相鄰的所有未被標記過的頂點加入隊列。
    隊列先進先出(FIFO)的特性可以達到廣度優先搜索尋找距離逐漸增大的效果。在深度優先搜索中,實際上隱式地使用了一個遵循後進先出(LIFO)規則的棧,在dfs的遞歸調用的過程中,這個棧由系統管理。
    在這裏插入圖片描述

兩種搜索方式的對比

不管是深度優先還是廣度優先搜索算法,它們都會先將起點存入數據結構中,然後重複以下步驟直到數據結構被清空:

  • 取其中的下一個頂點v並標記它
  • 將與v相鄰而又未被標記過的頂點加入數據結構中
    兩種算法的區別在於從數據結構中獲取下一個頂點的規則,深度優先搜索會首先取最晚加入數據結構的頂點,而廣度優先搜索取得則是最早加入的頂點。這種規則的區別會影響搜索圖的路徑,深度優先搜索會不斷深入圖中,並在棧中保存了所有分叉的頂點,廣度優先搜索則像扇面一般掃描圖,用一個隊列保存訪問過的最前段的頂點。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章