算法概要:
解決(不含負權邊的加權有向圖的)單點最短路徑問題。計算的結果是一棵最短路徑樹(SPT,最短路徑樹)。主要特點是以起始點爲中心向外層層擴展,直到擴展到終點爲止。
維基百科的描述:
這個算法是通過爲每個頂點 v 保留目前爲止所找到的從s到v的最短路徑來工作的。初始時,原點 s 的路徑長度值被賦爲 0 (d[s] = 0),若存在能直接到達的邊(s,m),則把d[m]設爲w(s,m),同時把所有其他(s不能直接到達的)頂點的路徑長度設爲無窮大,即表示我們不知道任何通向這些頂點的路徑(對於 V 中所有頂點 v 除 s 和上述 m 外 d[v] = ∞)。當算法結束時,d[v] 中存儲的便是從 s 到 v 的最短路徑,或者如果路徑不存在的話是無窮大。 Dijkstra 算法的基礎操作是邊的拓展:如果存在一條從 u 到 v 的邊,那麼從 s 到 v 的最短路徑可以通過將邊(u, v)添加到尾部來拓展一條從 s 到 v 的路徑。這條路徑的長度是 d[u] + w(u, v)。如果這個值比目前已知的 d[v] 的值要小,我們可以用新值來替代當前 d[v] 中的值。拓展邊的操作一直運行到所有的 d[v] 都代表從 s 到 v 最短路徑的花費。這個算法經過組織因而當 d[u] 達到它最終的值的時候每條邊(u, v)都只被拓展一次。
算法維護兩個頂點集 S 和 Q。集合 S 保留了我們已知的所有 d[v] 的值已經是最短路徑的值頂點,而集合 Q 則保留其他所有頂點。集合S初始狀態爲空,而後每一步都有一個頂點從 Q 移動到 S。這個被選擇的頂點是 Q 中擁有最小的 d[u] 值的頂點。當一個頂點 u 從 Q 中轉移到了 S 中,算法對每條外接邊 (u, v) 進行拓展。
實現描述:
其實Dijkstra算法跟Prim算法有很多相似之處。
Prim算法每次選擇離MST距離最短的一個頂點,判斷是否要將它加入MST中。Dijkstra算法每次選擇離初始節點距離最短的一個頂點,判斷是否要將它加入SPT中。這一點在代碼中的體現就是relax方法。
SP方法:**Dijkstra算法選取不同的初始節點,生成的SPT也不一樣。所以我實現**SP方法的時候提供了一個形參。每次調用SP方法,它都會先清除上一次的結果。
relax方法:遍歷從頂點v指出的所有邊(指向Wi),如果加入該邊後從初始節點(s)到Wi的距離可以變得更短,則暫時把它加入SPT中。
測試用的圖:
package ShortestPath;
import java.util.Stack;
import list.IndexMinPQ;
public class Dijkstra {
private EdgeWeightDiGraph g;//加權有向圖
private int s;//初始節點
private int[] from;
private double[] dist;//索引位置的頂點距離初始節點的距離
private IndexMinPQ<Double> pq;
public Dijkstra(EdgeWeightDiGraph g, int s) {
this.g = g;
this.s = s;
from = new int[g.V()];
dist = new double[g.V()];
for(int v=0;v<g.V();v++)
dist[v] = Double.POSITIVE_INFINITY;
pq = new IndexMinPQ<>(g.V());
sp(s);//使用Dijkstra生成最短路徑樹
}
/**
* 從頂點startVertex開始生成最短路徑樹
* @param startVertex
*/
public void sp(int startVertex){
clear();//清除之前的最短路徑樹的信息
this.s = startVertex;//更新初始節點
dist[startVertex] = 0.0;
pq.insert(startVertex, 0.0);
while(!pq.isEmpty())
relax(pq.delMin());
}
/**
* 放鬆頂點v,並將放鬆後的結果插入索引優先隊列pq
* @param v
*/
public void relax(int v){
for(DirectedEdge e:g.adj(v)){//遍歷從v指出的邊
int w = e.to();
if(dist[w] > dist[v] + e.weight()){//如果有更短路徑,則更新from和dist
dist[w] = dist[v] + e.weight();
from[w] = v;
if(pq.contains(w))//然後更新pq
pq.changeKey(w, dist[w]);
else
pq.insert(w, dist[w]);
}
}
}
/**
* 清除之前的最短路徑樹的信息
*/
private void clear(){
from = new int[g.V()];
dist = new double[g.V()];
for(int v=0;v<g.V();v++)
dist[v] = Double.POSITIVE_INFINITY;
pq = new IndexMinPQ<>(g.V());
}
public double distTo(int w){
return dist[w];
}
public boolean hasPathTo(int w){
return dist[w] != Double.POSITIVE_INFINITY;
}
public Iterable<Integer> pathTo(int w){
if(!hasPathTo(w))
return null;
Stack<Integer> path = new Stack<>();
for(int x=w; x!=s; x=from[x])
path.push(x);
path.push(s);
return path;
}
public static void main(String[] args) {
EdgeWeightDiGraph g =new EdgeWeightDiGraph(8);
DirectedEdge e1 = new DirectedEdge(4, 5, 0.35);
DirectedEdge e2 = new DirectedEdge(5, 4, 0.35);
DirectedEdge e3 = new DirectedEdge(4, 7, 0.37);
DirectedEdge e4 = new DirectedEdge(5, 7, 0.28);
DirectedEdge e5 = new DirectedEdge(7, 5, 0.28);
DirectedEdge e6 = new DirectedEdge(5, 1, 0.32);
DirectedEdge e7 = new DirectedEdge(0, 4, 0.38);
DirectedEdge e8 = new DirectedEdge(0, 2, 0.26);
DirectedEdge e9 = new DirectedEdge(7, 3, 0.39);
DirectedEdge e10 = new DirectedEdge(1, 3, 0.29);
DirectedEdge e11 = new DirectedEdge(2, 7, 0.34);
DirectedEdge e12 = new DirectedEdge(6, 2, 0.40);
DirectedEdge e13 = new DirectedEdge(3, 6, 0.52);
DirectedEdge e14 = new DirectedEdge(6, 0, 0.58);
DirectedEdge e15 = new DirectedEdge(6, 4, 0.93);
g.addEdge(e1 );
g.addEdge(e2 );
g.addEdge(e3 );
g.addEdge(e4 );
g.addEdge(e5 );
g.addEdge(e6 );
g.addEdge(e7 );
g.addEdge(e8 );
g.addEdge(e9 );
g.addEdge(e10);
g.addEdge(e11);
g.addEdge(e12);
g.addEdge(e13);
g.addEdge(e14);
g.addEdge(e15);
Dijkstra d = new Dijkstra(g,0);
Stack<Integer> stack = new Stack<>();
stack = (Stack<Integer>) d.pathTo(6);
System.out.println("頂點" + "\t" + "距離");
while(!stack.isEmpty()){
int a = stack.pop();
System.out.print(a + "\t");
System.out.println(d.distTo(a));
}
}
}
結果:
總結:
在一幅含有V個頂點和E條邊的加權有向圖中,使用Dijkstra算法計算SPT的空間複雜度爲O(v), 時間複雜度爲O(ElogV)