最短路径算法——Dijkstra,Bellman-Ford,Floyd-Warshall,Johnson,无一幸免
本文内容框架:
§1 Dijkstra算法
§2 Bellman-Ford算法
§3 Floyd-Warshall算法
§4 Johnson算算法
§5 问题归约
§6 小结
常用的最短路径算法有:Dijkstra算法、Bellman-Ford算法、Floyd-Warshall算法、Johnson算法
最短路径算法可以分为单源点最短路径和全源最短路径。
单源点最短路径有Dijkstra算法和Bellman-Ford算法,其中Dijkstra算法主要解决所有边的权为非负的单源点最短路径,Bellman-Ford算法可以适用权值有负值的问题。
全源最短路径主要有Floyd-Warshall算法和Johnson算法,其中Floyd算法可以检测图中的负环并可以解决不包括负环的图中全源最短路径问题,Johnson算法相比Floyd-Warshall算法,效率更高。
算法性能分析
在分别讲解这四个算法之前先来理清下这个四个算法的复杂度:Dijkstra算法直接实现时间复杂度是O(n²),空间复杂度是O(n)(保存距离和路径),二叉堆实现时间复杂度变成O((V+E)logV),Fibonacci Heap可以将复杂度降到O(E+VlogV);Bellman-Ford算法时间复杂度是O(V*E),SPFA是时间复杂度是O(kE);Floyd-Warshall算法时间复杂度是O(n³),空间复杂度是O(n²);Johnson算法时间复杂度是O( V * E * lgd(V) ),比Floyd-Warshall算法效率高。
最短路径算法之Dijkstra算法
╔
§1 Dijkstra算法
Dijkstra算法思想
Dijkstra算法思想为:设G=(V,E)是一个带权有向图(无向可以转化为双向有向),把图中顶点集合V分成两组,第一组为已求出最短路径的顶点集合(用S表示,初始时S中只有一个源点,以后每求得一条最短路径 , 就将 加入到集合S中,直到全部顶点都加入到S中,算法就结束了),第二组为其余未确定最短路径的顶点集合(用U表示),按最短路径长度的递增次序依次把第二组的顶点加入S中。在加入的过程中,总保持从源点v到S中各顶点的最短路径长度不大于从源点v到U中任何顶点的最短路径长度。此外,每个顶点对应一个距离,S中的顶点的距离就是从v到此顶点的最短路径长度,U中的顶点的距离,是从v到此顶点只包括S中的顶点为中间顶点的当前最短路径长度。
Dijkstra算法具体步骤
(1)初始时,S只包含源点,即S={v},v的距离dist[v]为0。U包含除v外的其他顶点,U中顶点u距离dis[u]为边上的权值(若v与u有边) )或∞(若u不是v的出边邻接点即没有边
(2)从U中选取一个距离v(dist[k])最小的顶点k,把k,加入S中(该选定的距离就是v到k的最短路径长度)。
(3)以k为新考虑的中间点,修改U中各顶点的距离;若从源点v到顶点u(u∈ U)的距离(经过顶点k)比原来距离(不经过顶点k)短,则修改顶点u的距离值,修改后的距离值的顶点k的距离加上边上的权(即如果dist[k]+w[k,u] (4)重复步骤(2)和(3)直到所有顶点都包含在S中(要循环n-1次)。 ╝① Dijkstra算法实现 ╔ 直接实现 最简单的实现方法就是,在每次循环中,再用一个循环找距离最短的点,然后用任意的方法更新与其相邻的边,时间复杂度显然为O(n²) 对于空间复杂度:如果只要求出距离,只要n的附加空间保存距离就可以了(距离小于当前距离的是已访问的节点,对于距离相等的情况可以比较编号或是特殊处理一下)。如果要求出路径则需要另外V的空间保存前一个节点,总共需要2n的空间。 ╝② ╔ ╝⑤ ╔ 二叉堆实现 使用二叉堆(Binary Heap)来保存没有扩展过的点的距离并维护其最小值,并在访问每条边的时候更新,可以把时间复杂度变成O((V+E)logV)。 当边数远小于点数的平方时,这种算法相对来说有很好的效果。但是当E=O(V2)时(有时候表现为不限制边的条数),用二叉堆的优化反倒会更慢。因为此时的复杂度是O(V+V*2logV),小于不用堆的实现的O(n²)的复杂度。 另外此时要用邻接表保存边,使得扩展边的总复杂度为O(E),否则复杂度不会减小。 空间复杂度:这种算法需要一个二叉堆,及其反向指针,另外还要保存距离,所以所用空间为3V。如果保存路径则为4V。 具体思路:先将所有的点插入堆,并将值赋为极大值(maxint/maxlongint),将原点赋值为0,通过松弛技术(relax)进行更新以及设定为扩展。 ╝② ╔ ╝④ 再献上一个实现 ╔ ╝③ ╔ 菲波那契堆实现 用类似的方法,使用Fibonacci Heap可以将复杂度降到O(E+VlogV),但实现比较麻烦。因此这里暂不列举。 ╝② 最短路径算法之Bellman-Ford算法 §2 Bellman-Ford算法 ╔ Bellman-Ford算法思想 Bellman-Ford算法能在更普遍的情况下(存在负权边)解决单源点最短路径问题。对于给定的带权(有向或无向)图 G=(V,E),其源点为s,加权函数 w是 边集 E 的映射。对图G运行Bellman-Ford算法的结果是一个布尔值,表明图中是否存在着一个从源点s可达的负权回路。若不存在这样的回路,算法将给出从源点s到 图G的任意顶点v的最短路径d[v]。 Bellman-Ford算法流程: (1) 初始化:将除源点外的所有顶点的最短距离估计值 d[v] ←+∞, d[s] ←0; (2) 迭代求解:反复对边集E中的每条边进行松弛操作,使得顶点集V中的每个顶点v的最短距离估计值逐步逼近其最短距离;(运行|v|-1次) (3) 检验负权回路:判断边集E中的每一条边的两个端点是否收敛。如果存在未收敛的顶点,则算法返回false,表明问题无解;否则算法返回true,并且从源点可达的顶点v的最短距离保存在 d[v]中。 算法描述如下: Bellman-Ford(G,w,s) :boolean //图G ,边集 函数 w ,s为源点 1 for each vertex v ∈ V(G) do //初始化 1阶段 2 d[v] ←+∞ 3 d[s] ←0; //1阶段结束 4 for i=1 to |v|-1 do //2阶段开始,双重循环。 5 for each edge(u,v) ∈E(G) do //边集数组要用到,穷举每条边。 6 If d[v]> d[u]+ w(u,v) then //松弛判断 7 d[v]=d[u]+w(u,v) //松弛操作 2阶段结束 8 for each edge(u,v) ∈E(G) do 9 If d[v]> d[u]+ w(u,v) then 10 Exit false 11 Exit true 下面给出描述性证明: 首先指出,图的任意一条最短路径既不能包含负权回路,也不会包含正权回路,因此它最多包含|v|-1条边。 其次,从源点s可达的所有顶点如果 存在最短路径,则这些最短路径构成一个以s为根的最短路径树。Bellman-Ford算法的迭代松弛操作,实际上就是按顶点距离s的层次,逐层生成这棵最短路径树的过程。 在对每条边进行1遍松弛的时候,生成了从s出发,层次至多为1的那些树枝。也就是说,找到了与s至多有1条边相联的那些顶点的最短路径;对每条边进行第2遍松弛的时候,生成了第2层次的树枝,就是说找到了经过2条边相连的那些顶点的最短路径……。因为最短路径最多只包含|v|-1 条边,所以,只需要循环|v|-1 次。 每实施一次松弛操作,最短路径树上就会有一层顶点达到其最短距离,此后这层顶点的最短距离值就会一直保持不变,不再受后续松弛操作的影响。(但是,每次还要判断松弛,这里浪费了大量的时间,怎么优化?单纯的优化是否可行?) 如果没有负权回路,由于最短路径树的高度最多只能是|v|-1,所以最多经过|v|-1遍松弛操作后,所有从s可达的顶点必将求出最短距离。如果 d[v]仍保持 +∞,则表明从s到v不可达。 如果有负权回路,那么第 |v|-1 遍松弛操作仍然会成功,这时,负权回路上的顶点不会收敛。 ╝⑥ Bellman-Ford算法实现 ╔ ╝⑦ Bellman-Ford算法优化——SPFA算法 循环的提前跳出:在实际操作中,贝尔曼-福特算法经常会在未达到V-1次前就出解,V-1其实是最大值。于是可以在循环中设置判定,在某次循环不再进行松弛时,直接退出循环,进行负权环判定。 具体做法是用一个队列保存待松弛的点,然后对于每个出队的点依次遍历每个与他有边相邻的点(用邻接表效率较高),如果该点可以松弛并且队列中没有该点则将它加入队列中(只有进行松弛操作的点才会对它的邻接点有影响,也就是说其邻接点才需要松弛操作),如此迭代直到队列为空。 SPFA算法实现 ╔ ╝⑧ 最短路径算法之Floyd-Warshall算法 §3 Floyd-Warshall算法 ╔ Floyd-Warshall算法是解决任意两点间的最短路径的算法,可以处理有向图或负权值的最短路径问题,同时也被用于计算有向图的传递闭包。算法的时间复杂度为O(n³),空间复杂度为O(n²)。 Floyd-Warshall算法的原理是动态规划。 设为从到的只以集合中的节点为中间節点的最短路径的长度。 因此,。 在实际算法中,为了节约空间,可以直接在原来空间上进行迭代,这样空间可降至二维。 ╝⑨ Floyd-Warshall算法实现 ╔ 如果dist[i][k]或者dist[k][j]不存在,程序中用∞代替。 真正的Floyd算法是一种基于DP(Dynamic Programming)的最短路径算法。 设图G中n 个顶点的编号为1到n。令c [i, j, k]表示从i 到j 的最短路径的长度,其中k 表示该路径中的最大顶点,也就是说c[i,j,k]这条最短路径所通过的中间顶点最大不超过k。因此,如果G中包含边,则c[i, j, 0] =边 的长度;若i= j ,则c[i,j,0]=0;如果G中不包含边,则c (i, j, 0)= +∞。c[i, j, n] 则是从i 到j 的最短路径的长度。 对于任意的k>0,通过分析可以得到:中间顶点不超过k 的i 到j 的最短路径有两种可能:该路径含或不含中间顶点k。若不含,则该路径长度应为c[i, j, k-1],否则长度为 c[i, k, k-1] +c [k, j, k-1]。c[i, j, k]可取两者中的最小值。 状态转移方程:c[i, j, k]=min{c[i, j, k-1], c [i, k, k-1]+c [k, j, k-1]},k>0。 这样,问题便具有了最优子结构性质,可以用动态规划方法来求解。 ╝⑨ 最短路径算法之Johnson算法 §4 Johnson算算法 Johson算法是目前最高效的在无负环可带负权重的网络中求所有点对最短路径的算法. Johson算法是Bellman-Ford算法, Reweighting(重赋权重)和Dijkstra算法的大综合. 对每个顶点运用Dijkstra算法的时间开销决定了Johnson算法的时间开销. 每次Dijkstra算法(d堆PFS实现)的时间开销是O( E * lgd(V) ). 其中E为边数, V为顶点数, d为采用d路堆实现优先队列ADT. 所以, 此种情况下Johnson算法的时间复杂度是O( V * E * lgd(V) )。 Johnson算法具体步骤(翻译自wikipedia): ╔ 1.初始化,把一个node q添加到图G中,使node q 到图G每一个点的权值为0。 2.使用Bellman-Ford算法,从源点为q,寻找每一个点 v从q到v的最短路径h(v),如果存在负环的话,算法终止。 3.使用第2步骤中Bellman-Ford计算的最短路径值对原来的图进行reweight操作(重赋值):边的权值w(u,v),修改成w(u,v)+h(u)-h(v)。 4.最后,移去q,针对新图(重赋值之后的图)使用Dijkstra算法计算从每一个点s到其余另外点的最短距离。 ╝⑩ Johnson算法实现: ╔ ╝⑩+1 §5 问题归约 ╔ 对于两个问题A和B,如果使用求解B的一个算法来开发一个求解A的算法,且最坏的情况下算法总时间不会超过最坏情况下求解B的算法运行时间的常量倍,则称问题A可归约(reduce)为问题B。 1.传递闭包问题可归约为有非负权值的所有对最短路径问题。 给定两点u和v,有向图中从u到v存在一条路径,当且仅当网中从u到v的路径长度非零。 2.在边权没有限制的网中,(单源点或所有对)最长路径和最短路径问题是等价的。 3.作业调度问题可归约为差分约束问题。 4.有正常数的差分约束问题等价于无环网中的单源点最长路径。 5.带有截止期的作业调度问题可归约为(允许带有负权值的)最短路径问题。 ╝⑩+2 §6 最短路径的扩展与应用 1.k短路 2.差分约束系统 3.DAG图上的单源点最短路径 4.Flyod求最小环 §6 小结 这篇文章把最短路径的四个算法——Dijkstra,Bellman-Ford,Floyd-Warshall,Johnson从原理到步骤,再从流程到实现都将了,有了一定的认识和理解。如果你有任何建议或者批评和补充,请留言指出,不胜感激,更多参考请移步互联网。 参考: ①永远的绿岩:http://2728green-rock.blog.163.com/blog/static/43636790200901211848284/ ②NOCOW:http://www.nocow.cn/index.php/Dijkstra%E7%AE%97%E6%B3%95 ③oa414:http://www.cnblogs.com/oa414/archive/2011/07/25/2115858.html ④NOCOW:http://www.nocow.cn/index.php/Dijkstra_%E4%BA%8C%E5%8F%89%E5%A0%86%E5%AE%9E%E7%8E%B0_C ⑤IT_元帅:http://www.cnblogs.com/newwy/archive/2010/11/20/1882569.html ⑥infinity:http://www.cppblog.com/infinity/archive/2008/11/11/66621.html ⑦niushuai666:http://blog.csdn.net/niushuai666/article/details/6791765 ⑧Lost:http://www.cnblogs.com/zhuangli/archive/2008/07/26/1251869.html ⑨极限定律:http://www.cppblog.com/mythit/archive/2009/04/21/80579.html ⑩wikipedia:http://en.wikipedia.org/wiki/Johnson's_algorithm ⑩+1pleasetojava:http://pleasetojava.iteye.com/blog/1270377 ⑩+2Robert Sedgewick:Algorithm in C/*********************************
* 最短路径---Dijkstra算法实现
* HDU:2544
* BLOG:www.cnblogs.com/newwy
* AUTHOR:Wang Yong
**********************************/
#include
int GraphDijk(struct Graph *g, int root, int *parent, int *distance)
{
// 将除根结点之外的点都放入堆中,设置所有键为INFINITY
// 遍历根结点发出的边,将其最短路径设为相应权值,并维持堆性质
// RemoveTop,此结点已经取最短路径,如果为INFINITY,则终止算法
// 否则,将其状态设为已标记,并设为根结点
// loop back
parent[root] = root;
int reflection[g->V];
int heap_real[g->V - 1];
for (int i=0,j=0; i < g->V; i++) {
if (i == root) {
distance[i] = 0;
} else {
distance[i] = INFINITY;
heap_real[j++] = i;
reflection[i] = j;
}
}
struct Edge *e;
struct list_t *iter;
int *heap = heap_real - 1;
int base = 0; /* euqal to distance[root] */
int size = g->V - 1;
int length;
do {
iter = list_next(&(g->vertices + root)->link);
for (; iter; iter = list_next(iter)) {
e = list_entry(iter, struct Edge, link);
length = base + e->weight;
if (length < distance[e->to]) {
HeapDecreaseKey(heap, size,
distance, reflection,
reflection[e->to], length);
parent[e->to] = root;
}
}
root = HeapRemoveTop(heap, size, distance, reflection);
base = distance[root];
if (distance[root] == INFINITY) {
/* remain nodes in heap is not accessible */
return g->V - (size + 1); /* 返回强连通分支结点数 */
}
} while (size);
/* successfull end algorightm */
return g->V;
}
/*很裸很水的最短路,练习二叉堆优化的Dijstra~
之前二叉堆优化的Prim敲了好几遍前后花了不下八个小时调试还是没有调试成功,
但是还好,熟悉了优先队列的操作。
十几天后的今天重新想起这个,终于调出来了堆优化的Dijstra。理解之后还是蛮简单的。
一些写法并不是最优的,例如heap的实现中可以减少交换元素等。但是有这个自己写的AC
过的Dijkstra在,以后写二叉堆优化的Prim/Dijkstra和其它优先队列的题目就可以拿它对照着Debug了。
2011-07-24 23:00
*/
#include
#include
#include
for(int k =1 ; k <= n ; k ++ ){
for(int i =1 ; i<= n ; i++){
for(int j =1 ;j<=n;j++){
dist[ i ][ j ]= min( dist[ i ][ j ],dist[ i ][ k ]+dist[ k ][ j ] );
}
}
}
#include
#include "stdafx.h"
#include