算法概要:
解决(不含负权边的加权有向图的)单点最短路径问题。计算的结果是一棵最短路径树(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)