兩種經典的單源最短路徑算法圖解與java實現(貪心Dijkstra和A*)

單源最短路徑典型的啓發式搜索有兩種,分別是貪婪最佳優先搜索(Greedy best-first search)和A*尋路搜索。這篇文章以最短路徑問題爲例來展開討論兩種搜索方法的思路。

First Step:確定路徑的存儲結構

可求最短路徑的結構往往是有向帶權圖,用代碼表示就是

public class Graph { // 有向有權圖的鄰接表表示
  private LinkedList<Edge> adj[]; // 鄰接表
  private int v; // 頂點個數
  public Graph(int v) {
    this.v = v;
    this.adj = new LinkedList[v];
    for (int i = 0; i < v; ++i) {
      this.adj[i] = new LinkedList<>();
    }
  }
  public void addEdge(int s, int t, int w) { // 添加一條邊
    this.adj[s].add(new Edge(s, t, w));
  }
  private class Edge {
    public int sid; // 邊的起始頂點編號
    public int tid; // 邊的終止頂點編號
    public int w; // 權重
    public Edge(int sid, int tid, int w) {
      this.sid = sid;  this.tid = tid;  this.w = w;
    }
  }
  //這個類是爲了dijkstra實現用的
  private class Vertex {
    public int id; // 頂點編號ID
    public int dist; // 從起始頂點到這個頂點的距離
    public Vertex(int id, int dist) {
      this.id = id;
      this.dist = dist;
    }
  }
}

Second Step:不同尋路算法實現

1.貪婪最佳優先搜索——Dijkstra算法

  用貪心思路,每一步都選最優。不足之處有:(1)沒辦法保證全局最優,甚至可能選入死循環路徑。(2)時間複雜度和空間複雜度最壞情況都可達到O(b^m),b是所有結點總數,m是搜索空間的最大深度

Dijkstra算法是貪心/BFS的升級版。當一個圖中的每條邊都加上權值後,BFS就沒辦法求一個點到另一個點的最短路徑了。這時候,需要用到Dijkstra算法。從最基本原理上講,把BFS改成Dijkstra算法,只需要把“隊列”改成“優先隊列”就可以了。
四個核心參數:
vertexs[i]:V0到Vi的最短路徑長度(或權重)。
predecessor[i]:存當前最短路徑上的倒數第二個結點。
PriorityQueue :每一位存Vi以及vertexs[i],會自動根據vertex[i]的值排序,小頂堆。
inqueue[i]:bool變量,Vi計入最短路徑則爲1,不計入爲0;
在這裏插入圖片描述
圖片來源:BFS算法講解

// 因爲Java提供的優先級隊列,沒有暴露更新數據的接口,所以我們需要重新實現一個
private class PriorityQueue { // 根據vertex.dist構建小頂堆
  private Vertex[] nodes;
  private int count;
  public PriorityQueue(int v) {
    this.nodes = new Vertex[v+1];
    this.count = v;
  }
}
public void dijkstra(int s, int t) { // 從頂點s到頂點t的最短路徑
  int[] predecessor = new int[this.v]; // 用來還原最短路徑
  Vertex[] vertexes = new Vertex[this.v];
  for (int i = 0; i < this.v; ++i) {
    vertexes[i] = new Vertex(i, Integer.MAX_VALUE);
  }
  PriorityQueue queue = new PriorityQueue(this.v);// 小頂堆
  boolean[] inqueue = new boolean[this.v]; // 標記是否進入過隊列
  vertexes[s].dist = 0;
  queue.add(vertexes[s]);
  inqueue[s] = true;
  while (!queue.isEmpty()) {
    Vertex minVertex= queue.poll(); // 取堆頂元素並刪除
    if (minVertex.id == t) break; // 最短路徑產生了
    for (int i = 0; i < adj[minVertex.id].size(); ++i) {
      Edge e = adj[minVertex.id].get(i); // 取出一條minVetex相連的邊
      Vertex nextVertex = vertexes[e.tid]; // minVertex-->nextVertex
      if (minVertex.dist + e.w < nextVertex.dist) { // 更新next的dist
        nextVertex.dist = minVertex.dist + e.w;
        predecessor[nextVertex.id] = minVertex.id;
        if (inqueue[nextVertex.id] == true) {
          queue.update(nextVertex); // 更新隊列中的dist值
        } else {
          queue.add(nextVertex);
          inqueue[nextVertex.id] = true;
        }
      }
    }
  }
  // 輸出最短路徑
  System.out.print(s);
  print(s, t, predecessor);
}
private void print(int s, int t, int[] predecessor) {
  if (s == t) return;
  print(s, predecessor[t], predecessor);
  System.out.print("->" + t);
}

2.A*算法

Dijkstra算法是用一個優先隊列來記錄遍歷的頂點與相應路徑長度,如果頂點到起點的路徑約短,就越先從隊列中取出來做擴展,雖然最後在整個圖裏找到最有路徑,但是開頭難免曲折。(最優路徑可能開始的邊權重很大)
快速尋路的A算法應運而生,不足是它只能找到次優解,不能保證最優。
A
算法評估函數公式f(i) = g(i) + h(i) ; 而Dijkstra算法是f(i) = g(i) ;
其中g(i)是從起始到當前結點n的最小代價值 ,h(i)是從當前到目標結點路徑的最小代價值,f(i)是經過結點i,具有最小代價值的路徑。
其中h(i)一般可以通過
A* 算法的代碼實現的主要邏輯是下面這段代碼。它跟 Dijkstra 算法的代碼實現,主要有 3 點區別:
優先級隊列構建的方式不同。A* 算法是根據 f 值(也就是剛剛講到的 f(i)=g(i)+h(i))來構建優先級隊列,而 Dijkstra 算法是根據 dist 值(也就是剛剛講到的 g(i))來構建優先級隊列;

public void astar(int s, int t) { // 從頂點s到頂點t的路徑
  int[] predecessor = new int[this.v]; // 用來還原路徑
  // 按照vertex的f值構建的小頂堆,而不是按照dist
  PriorityQueue queue = new PriorityQueue(this.v);
  boolean[] inqueue = new boolean[this.v]; // 標記是否進入過隊列
  vertexes[s].dist = 0;
  vertexes[s].f = 0;
  queue.add(vertexes[s]);
  inqueue[s] = true;
  while (!queue.isEmpty()) {
    Vertex minVertex = queue.poll(); // 取堆頂元素並刪除
    for (int i = 0; i < adj[minVertex.id].size(); ++i) {
      Edge e = adj[minVertex.id].get(i); // 取出一條minVetex相連的邊
      Vertex nextVertex = vertexes[e.tid]; // minVertex-->nextVertex
      if (minVertex.dist + e.w < nextVertex.dist) { // 更新next的dist,f
        nextVertex.dist = minVertex.dist + e.w;
        nextVertex.f 
           = nextVertex.dist+hManhattan(nextVertex, vertexes[t]);
        predecessor[nextVertex.id] = minVertex.id;
        if (inqueue[nextVertex.id] == true) {
          queue.update(nextVertex);
        } else {
          queue.add(nextVertex);
          inqueue[nextVertex.id] = true;
        }
      }
      if (nextVertex.id == t) { // 只要到達t就可以結束while了
        queue.clear(); // 清空queue,才能推出while循環
        break; 
      }
    }
  }
  // 輸出路徑
  System.out.print(s);
  print(s, t, predecessor); // print函數請參看Dijkstra算法的實現
}

A* 算法在更新頂點 dist 值的時候,會同步更新 f 值;
循環結束的條件也不一樣。Dijkstra 算法是在終點出隊列的時候才結束,A* 算法是一旦遍歷到終點就結束。
A* 算法之所以不能像 Dijkstra 算法那樣,找到最短路徑,主要原因是兩者的 while 循環結束條件不一樣。剛剛我們講過,Dijkstra 算法是在終點出隊列的時候才結束,A* 算法是一旦遍歷到終點就結束。對於 Dijkstra 算法來說,當終點出隊列的時候,終點的 dist 值是優先級隊列中所有頂點的最小值,即便再運行下去,終點的 dist 值也不會再被更新了。對於 A* 算法來說,一旦遍歷到終點,我們就結束 while 循環,這個時候,終點的 dist 值未必是最小值。

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