单源最短路径-迪杰斯特拉算法(Dijkstra's algorithm)
Dijkstra’s algorithm
迪杰斯特拉算法是目前已知的解决单源最短路径问题的最快算法.
单源(single source)最短路径,就是从一个源点出发,考察它到任意顶点所经过的边的权重之和为最小的路径.
迪杰斯特拉算法不能处理权值为负数或为零的边,因为本质上它是一种贪心算法,出现了负数意味着它可能会舍弃一条正确的边,而选择一个长边和一个负数边,因为长边和负数边的权值之和可能小于那条正确的边.
算法描述
它的过程也很简单,按照广度遍历的方式考察每一条有向边(v,w),如果可以对边进行松弛,就记录这条边,并更新到边的目标顶点的最短距离.
注意,这个“最短距离”是当前搜索进程中已知的最短距离而不一定是最终的最短距离.
对边进行松弛的操作如下:
/**
* 对边进行松弛
*
* 对一条有向边(v,w,weight)进行考察:
* 如果当前已知的到w的距离distance_to(w) > distance_to(v) + weight,
* 说明可以改善到w的距离.从而更新这个距离:
* distance_to(w) = distance_to(v) + weight;
* 同时选择这条边为到w的目前已知的最短边
* last_edge_to(w) = (v,w,weight)
*
* @param edge 有向带权边
*/
private void relaxEdge(WeightedEdge edge) {
IndexPriorityQueue<Double> q = indexCrossingEdges;
int src = edge.src;
int to =edge.to;
if(distanceTo[to] > distanceTo[src] + edge.weight) {
distanceTo[to] = distanceTo[src] + edge.weight;
lastEdgeTo[to] = edge;
if(q.contains(to))
q.decreaseKey(to,distanceTo[to]);
else
q.offer(to,distanceTo[to]);
}
}
松弛操作中,使用了索引式优先队列来获取平均O(1)的插入效率和O(logN)的降权(这里是最小优先队列,所以decrease-key是提升优先级)效率,请参考:索引式优先队列
控制算法的搜索方向的是一个循环,这个循环考察队列是否为空以判断是否所有的边都得到了处理.
同时,在下面的代码中也可以看出,搜索的方向总是从源点s出发,遍历它的邻接表,当耗尽邻接表时,从优先队列中出队和它最近的邻接点v,接着对v的邻接表进行搜索.
所以整个搜索方向看上去是在向广度方向进行的.
while (!q.isEmpty()) {
src = q.poll();
for(Edge e:g.vertices()[src].Adj) {
relaxEdge((WeightedEdge)e);
}
}
可以看出,迪杰斯特拉算法和求最小生成树的普利姆算法非常相似,因为它们都是一种基于广度优先的贪心算法.
关于普利姆算法的分析和实现,请参考:说说最小生成树
实现代码
下面给出迪杰斯特拉的完整实现:
/**
* Created by 浩然 on 4/21/15.
*/
public class Dijkstra {
/**
* 当前已知的最短距离,索引为顶点的编号
* 比如distanceTo[v]表示当前到顶点v的最短距离
*/
protected double[] distanceTo;
/**
* 当前已知的最短边,索引为顶点的编号
* 比如lastEdgeTo[v]表示当前到顶点v的最短边
*/
protected WeightedEdge[] lastEdgeTo;
/**
* 索引式优先队列,维护到顶点的最短距离
*/
protected IndexPriorityQueue<Double> indexCrossingEdges;
/**
* 有向带权图
*/
private WeightedDirectedGraph g;
private LinkedList<WeightedEdge> shortestPath;
/**
* 单源
*/
private int src;
public Dijkstra(WeightedDirectedGraph g){
this.g = g;
}
public void performSP(int src) {
this.src = src;
validateEdges();
resetMemo();
IndexPriorityQueue q = indexCrossingEdges;
//从源点开始
distanceTo[src] = 0;
q.offer(src,distanceTo[src]);
while (!q.isEmpty()) {
src = q.poll();
for(Edge e:g.vertices()[src].Adj) {
relaxEdge((WeightedEdge)e);
}
}
}
/**
* 对边进行松弛
*
* 对一条有向边(v,w,weight)进行考察:
* 如果当前已知的到w的距离distance_to(w) > distance_to(v) + weight,
* 说明可以改善到w的距离.从而更新这个距离:
* distance_to(w) = distance_to(v) + weight;
* 同时选择这条边为到w的目前已知的最短边
* last_edge_to(w) = (v,w,weight)
*
* @param edge 有向带权边
*/
private void relaxEdge(WeightedEdge edge) {
IndexPriorityQueue<Double> q = indexCrossingEdges;
int src = edge.src;
int to =edge.to;
if(distanceTo[to] > distanceTo[src] + edge.weight) {
distanceTo[to] = distanceTo[src] + edge.weight;
lastEdgeTo[to] = edge;
if(q.contains(to))
q.decreaseKey(to,distanceTo[to]);
else
q.offer(to,distanceTo[to]);
}
}
private void validateEdges() {
for(Vertex v:g.vertices()) {
for(Edge e:v.Adj) {
if(((WeightedEdge) e).weight < 0) {
throw new IllegalArgumentException("边的权值不能为负!");
}
}
}
}
private void resetMemo() {
int vertexCount = g.vertexCount();
//重置sp
shortestPath = new LinkedList<>();
//重置所有已知最短路径
lastEdgeTo = new WeightedEdge[vertexCount];
//重置所有距离
distanceTo = new double[vertexCount];
for(int i = 0; i < distanceTo.length; i++) {
distanceTo[i] = Double.POSITIVE_INFINITY;
}
//重置优先队列
indexCrossingEdges = new IndexPriorityQueue<>();
}
/**
* 从源点到目标点是否存在一条路径
* @param to 目标点
* @return 存在则返回真,否则返回假
*/
private boolean hasPathTo(int to) {
return distanceTo[to] < Double.POSITIVE_INFINITY;
}
public void printShortestPath(int to) {
if(!hasPathTo(to)){
System.out.println(String.format("%d-%d 不可达",src,to));
}
Stack<WeightedEdge> stack = new Stack<>();
for(Edge edge = lastEdgeTo[to]; edge != null; ){
WeightedEdge we = (WeightedEdge)edge;
stack.push(we);
edge = lastEdgeTo[we.src];
}
System.out.println(String.format("%d-%d的最短路径:",src,to));
while (!stack.isEmpty()) {
WeightedEdge we = stack.pop();
shortestPath.add(we);
System.out.println(String.format("%d-%d %.2f",we.src,we.to,we.weight));
}
}
}
算法的时间复杂度
对所有的边要进行考察,所以有O(E ).
每次考察中,要进行队列的入队或降权操作,队列中最多维护V条记录.最差为O(logV)
所以最差情况下,时间复杂度为O(ElogV).
使用斐波那契堆来代替二叉堆实现的优先队列理论上可以进行有限的优化,因为这种堆的降权(decrease-key)操作的摊还代价为O(1 ),但实际上,它过于长的常量时间并不一定能带来那么美的效率.