無向圖的基本算法


  根據性質,圖可以分為無向圖和有向圖。本文先介紹無向圖,后文再介紹有向圖。之所以要研究圖,是因為圖在生活中應用比較廣泛。

無向圖

  圖是若干個頂點(Vertices)和邊(Edges)相互連接組成的。邊僅由兩個頂點連接,並且沒有方向的圖稱為無向圖。在研究圖之前,有一些定義需要明確,下圖中表示了圖的一些基本屬性的含義,這里就不多說明。

 圖的API表示

  在研究圖之前,我們需要選用適當的數據結構來表示圖,有時候,我們常被我們的直覺欺騙,如下圖,這兩個其實是一樣的,這其實也是一個研究問題,就是如何判斷圖的形態。

   要用計算機處理圖,我們可以抽象出以下的表示圖的API:   Graph的API的實現可以由多種不同的數據結構來表示,最基本的是維護一系列邊的集合,

  如下:還可以使用鄰接矩陣來表示:

  也可以使用鄰接列表來表示: 

  由於采用如上方式具有比較好的靈活性,采用鄰接列表來表示的話,可以定義如下數據結構來表示一個Graph對象。

public class Graph { private readonly int verticals;//頂點個數
    private int edges;//邊的個數
    private List<int>[] adjacency;//頂點聯接列表
 
    public Graph(int vertical) { this.verticals = vertical; this.edges = 0; adjacency=new List<int>[vertical]; for (int v = 0; v < vertical; v++) { adjacency[v]=new List<int>(); } } public int GetVerticals () { return verticals; } public int GetEdges() { return edges; } public void AddEdge(int verticalStart, int verticalEnd) { adjacency[verticalStart].Add(verticalEnd); adjacency[verticalEnd].Add(verticalStart); edges++; } public List<int> GetAdjacency(int vetical) { return adjacency[vetical]; } }
View Code

  采用以上三種表示方式的效率如下:

  在討論完圖的表示之后,我們來看下在圖中比較重要的一種算法,即深度優先算法:

深度優先算法

  在談論深度優先算法之前,我們可以先看看迷宮探索問題。下面是一個迷宮和圖之間的對應關系:迷宮中的每一個交會點代表圖中的一個頂點,每一條通道對應一個邊。 迷宮探索可以采用Trémaux繩索探索法。即:

  • 在身后放一個繩子
  • 訪問到的每一個地方放一個繩索標記訪問到的交會點和通道
  • 當遇到已經訪問過的地方,沿着繩索回退到之前沒有訪問過的地方:

  圖示如下:

  下面是迷宮探索的一個小動畫:

  深度優先搜索算法模擬迷宮探索。在實際的圖處理算法中,我們通常將圖的表示和圖的處理邏輯分開來。所以算法的整體設計模式如下:

  • 創建一個Graph對象
  • 將Graph對象傳給圖算法處理對象,如一個Paths對象
  • 然后查詢處理后的結果來獲取信息

  下面是深度優先的基本代碼,我們可以看到,遞歸調用dfs方法,在調用之前判斷該節點是否已經被訪問過。

public class DepthFirstSearch {
    private boolean[] marked;    // marked[v] = is there an s-v path?
    private int count;           // number of vertices connected to s

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

    //depth first search from v
    private void dfs(Graph G, int v) {
        count++;
        marked[v] = true;
        for (int w : G.adj(v)) {
            if (!marked[w]) {
                dfs(G, w);
            }
        }
    }

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

    public int count() {
        return count;
    }
}
View Code

  試驗一個算法最簡單的辦法是找一個簡單的例子來實現。

深度優先路徑查詢

  有了這個基礎,我們可以實現基於深度優先的路徑查詢,要實現路徑查詢,我們必須定義一個變量來記錄所探索到的路徑。所以在上面的基礎上定義一個edgesTo變量來后向記錄所有到s的頂點的記錄,和僅記錄從當前節點到起始節點不同,我們記錄圖中的每一個節點到開始節點的路徑。為了完成這一日任務,通過設置edgesTo[w]=v,我們記錄從v到w的邊,換句話說,v-w是最后一條從s到達w的邊。edgesTo[]其實是一個指向其父節點的樹。

public class DepthFirstPaths
{
    private bool[] marked;//記錄是否被dfs訪問過
    private int[] edgesTo;//記錄最后一個到當前節點的頂點
    private int s;//搜索的起始點
 
    public DepthFirstPaths(Graph g, int s)
    {
        marked = new bool[g.GetVerticals()];
        edgesTo = new int[g.GetVerticals()];
        this.s = s;
        dfs(g, s);
    }
 
    private void dfs(Graph g, int v)
    {
        marked[v] = true;
        foreach (int w in g.GetAdjacency(v))
        {
            if (!marked[w])
            {
                edgesTo[w] = v;
                dfs(g,w);
            }
        }
    }
 
    public bool HasPathTo(int v)
    {
        return marked[v];
    }
 
    public Stack<int> PathTo(int v)
    {
 
        if (!HasPathTo(v)) return null;
        Stack<int> path = new Stack<int>();
 
        for (int x = v; x!=s; x=edgesTo[x])
        {
            path.Push(x);
        }
        path.Push(s);
        return path;
    }
}
View Code

  上圖中是黑色線條表示深度優先搜索中,所有定點到原點0的路徑,他是通過edgeTo[]這個變量記錄的,可以從右邊可以看出,他其實是一顆樹,樹根即是原點,每個子節點到樹根的路徑即是從原點到該子節點的路徑。下圖是深度優先搜索算法的一個簡單例子的追蹤。 

廣度優先算法

  通常我們更關注的是一類單源最短路徑的問題,那就是給定一個圖和一個源S,是否存在一條從s到給定頂點v的路徑,如果存在,找出最短的那條(這里最短定義為邊的條數最小)。深度優先算法是將未被訪問的節點放到一個棧中(stack),雖然在上面的代碼中沒有明確在代碼中寫stack,但是遞歸間接的利用遞歸堆實現了這一原理。和深度優先算法不同,廣度優先是將所有未被訪問的節點放到了隊列中。其主要原理是:

  • 將s放到FIFO中,並且將s標記為已訪問
  • 重復直到隊列為空
  1. 移除最近最近添加的頂點v
  2. 將v未被訪問的鄰接點添加到隊列中
  3. 標記他們為已經訪問

  廣度優先是以距離遞增的方式來搜索路徑的。

class BreadthFirstSearch
{
    private bool[] marked;
    private int[] edgeTo;
    private int sourceVetical;//Source vertical
 
    public BreadthFirstSearch(Graph g, int s)
    {
        marked=new bool[g.GetVerticals()];
        edgeTo=new int[g.GetVerticals()];
        this.sourceVetical = s;
        bfs(g, s);
    }
 
    private void bfs(Graph g, int s)
    {
        Queue<int> queue = new Queue<int>();
        marked[s] = true;
        queue.Enqueue(s);
        while (queue.Count()!=0)
        {
            int v = queue.Dequeue();
            foreach (int w in g.GetAdjacency(v))
            {
                if (!marked[w])
                {
                    edgeTo[w] = v;
                    marked[w] = true;
                    queue.Enqueue(w);
                }
            }
        }
    }
 
    public bool HasPathTo(int v)
    {
        return marked[v];
    }
 
    public Stack<int> PathTo(int v)
    {
        if (!HasPathTo(v)) return null;
 
        Stack<int> path = new Stack<int>();
        for (int x = v; x!=sourceVetical; x=edgeTo[x])
        {
            path.Push(x);
        }
        path.Push(sourceVetical);
        return path;
    }
 
}
View Code

  廣度優先算法的搜索步驟如下:

  廣度優先搜索首先是在距離起始點為1的范圍內的所有鄰接點中查找有沒有到達目標結點的對象,如果沒有,繼續前進在距離起始點為2的范圍內查找,依次向前推進。

連通分量

  使用深度優先遍歷計算圖的所有連通分量。

package Graph;

public class CC {
    private boolean[] marked;
    private int[] id;
    private int count;

    public CC(Graph graph, int s) {
        marked = new boolean[graph.V()];
        id = new int[graph.V()];
        for (int i =0; i < graph.V(); i++) {
            if (!marked(i)) {
                dfs(graph, i);
                count++;
            }
        }
    }

    private void dfs(Graph graph, int s) {
        marked[s] = true;
        id[s] = count;
        for (int W : graph.adj(s)) {
            if (marked(W))
                dfs(graph, W);
        }
    }

    //判斷v和W是否連通
    public boolean connected(int v, int w) {
        return id[v] == id[w];
    }
    //返回W所在的連通分量的標識符
    public int id(int v) {
        return id[v];
    }

    public boolean marked(int w) {
        return marked[w];
    }
    //連通分量
    public int count() {
        return count;
    }
}
View Code

總結

  本文簡要介紹了無向圖中的深度優先和廣度優先算法,這兩種算法時圖處理算法中的最基礎算法,也是后續更復雜算法的基礎。其中圖的表示,圖算法與表示的分離這種思想在后續的算法介紹中會一直沿用,下文將講解無向圖中深度優先和廣度優先的應用,以及利用這兩種基本算法解決實際問題的應用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM