最短路径算法

原网址:http://dsqiu.iteye.com/blog/1689163

http://blog.csdn.net/xu3737284/article/details/8973615
Bellman-Ford算法实现进行结点个数减一之后,再check所有的边,看是否有负权,有的话return false,break。。

最短路径算法——Dijkstra,Bellman-Ford,Floyd-Warshall,Johnson

最短路径算法——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的出边邻接点即没有边<v,u>)。

(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]<dist[u],那么把dist[u]更新成更短的距离dist[k]+w[k,u])。

(4)重复步骤(2)和(3)直到所有顶点都包含在S中(要循环n-1次)。

╝①

 

Dijkstra算法实现

直接实现

最简单的实现方法就是,在每次循环中,再用一个循环找距离最短的点,然后用任意的方法更新与其相邻的边,时间复杂度显然为O(n²)

对于空间复杂度:如果只要求出距离,只要n的附加空间保存距离就可以了(距离小于当前距离的是已访问的节点,对于距离相等的情况可以比较编号或是特殊处理一下)。如果要求出路径则需要另外V的空间保存前一个节点,总共需要2n的空间。

╝②

 

Cpp代码   收藏代码
  1. /********************************* 
  2. *   最短路径---Dijkstra算法实现  
  3. *      HDU:2544  
  4. *   BLOG:www.cnblogs.com/newwy 
  5. *   AUTHOR:Wang Yong 
  6. **********************************/  
  7. #include <iostream>  
  8. #define MAX 100  
  9. #define INF 1000000000  
  10. using namespace std;  
  11.  int dijkstra (int mat[][MAX],int n, int s,int f)  
  12.  {  
  13.      int dis[MAX];  
  14.      int mark[MAX];//记录被选中的结点   
  15.      int i,j,k = 0;  
  16.      for(i = 0 ; i < n ; i++)//初始化所有结点,每个结点都没有被选中   
  17.          mark[i] = 0;  
  18.     for(i = 0 ; i < n ; i++)//将每个结点到start结点weight记录为当前distance   
  19.     {  
  20.         dis[i] = mat[s][i];  
  21.         //path[i] = s;  
  22.     }  
  23.     mark[s] = 1;//start结点被选中   
  24.     //path[s] = 0;  
  25.     dis[s] = 0;//将start结点的的距离设置为0   
  26.     int min ;//设置最短的距离。   
  27.     for(i = 1 ; i < n; i++)  
  28.     {  
  29.         min = INF;  
  30.         for(j = 0 ; j < n;j++)  
  31.         {  
  32.             if(mark[j] == 0  && dis[j] < min)//未被选中的结点中,距离最短的被选中   
  33.             {  
  34.                 min = dis[j] ;  
  35.                 k = j;  
  36.             }  
  37.         }  
  38.         mark[k] = 1;//标记为被选中   
  39.         for(j = 0 ; j < n ; j++)  
  40.         {  
  41.             if( mark[j] == 0  && (dis[j] > (dis[k] + mat[k][j])))//修改剩余结点的最短距离   
  42.             {  
  43.                 dis[j] = dis[k] + mat[k][j];  
  44.             }  
  45.         }  
  46.     }  
  47.     return dis[f];      
  48.  }   
  49.  int mat[MAX][MAX];  
  50. int main()  
  51. {  
  52.     int n,m;  
  53.     while(scanf("%d %d",&n,&m))  
  54.     {  
  55.         int a,b,dis;  
  56.         if(n == 0 || m == 0)  
  57.             break;  
  58.         int i,j;  
  59.         for(i = 0 ; i < n;i++)  
  60.             for(j = 0 ; j < n; j++)  
  61.                 mat[i][j] = INF;  
  62.         for(i = 0 ; i < m ;i++)  
  63.         {  
  64.             scanf("%d %d %d",&a,&b,&dis);  
  65.             --a,--b;  
  66.             if(dis < mat[a][b] || dis < mat[b][a])  
  67.             mat[a][b] = mat[b][a] = dis;  
  68.         }  
  69.         int ans = dijkstra(mat,n,0,n-1);  
  70.         printf("%d\n",ans);  
  71.     }  
  72.    
  73. }  

 ╝⑤

二叉堆实现

使用二叉堆(Binary Heap)来保存没有扩展过的点的距离并维护其最小值,并在访问每条边的时候更新,可以把时间复杂度变成O((V+E)logV)。

当边数远小于点数的平方时,这种算法相对来说有很好的效果。但是当E=O(V2)时(有时候表现为不限制边的条数),用二叉堆的优化反倒会更慢。因为此时的复杂度是O(V+V*2logV),小于不用堆的实现的O(n²)的复杂度。

另外此时要用邻接表保存边,使得扩展边的总复杂度为O(E),否则复杂度不会减小。

空间复杂度:这种算法需要一个二叉堆,及其反向指针,另外还要保存距离,所以所用空间为3V。如果保存路径则为4V。

具体思路:先将所有的点插入堆,并将值赋为极大值(maxint/maxlongint),将原点赋值为0,通过松弛技术(relax)进行更新以及设定为扩展。

╝②

 

C代码   收藏代码
  1. int  GraphDijk(struct Graph *g, int root, int *parent, int *distance)  
  2. {  
  3.     // 将除根结点之外的点都放入堆中,设置所有键为INFINITY  
  4.     // 遍历根结点发出的边,将其最短路径设为相应权值,并维持堆性质  
  5.     // RemoveTop,此结点已经取最短路径,如果为INFINITY,则终止算法  
  6.     // 否则,将其状态设为已标记,并设为根结点  
  7.     // loop back  
  8.     parent[root] = root;  
  9.     int reflection[g->V];  
  10.     int heap_real[g->V - 1];  
  11.     for (int i=0,j=0; i < g->V; i++) {  
  12.         if (i == root) {  
  13.             distance[i] = 0;  
  14.         } else {  
  15.             distance[i] = INFINITY;  
  16.             heap_real[j++] = i;  
  17.             reflection[i] = j;  
  18.         }  
  19.     }  
  20.    
  21.     struct Edge *e;  
  22.     struct list_t *iter;  
  23.     int *heap = heap_real - 1;  
  24.     int base = 0;  /* euqal to distance[root]  */  
  25.     int size = g->V - 1;  
  26.     int length;  
  27.     do {  
  28.         iter = list_next(&(g->vertices + root)->link);  
  29.         for (; iter; iter = list_next(iter)) {  
  30.             e = list_entry(iter, struct Edge, link);  
  31.             length = base + e->weight;  
  32.             if (length < distance[e->to]) {   
  33.                 HeapDecreaseKey(heap, size,   
  34.                     distance, reflection,   
  35.                     reflection[e->to], length);  
  36.                 parent[e->to] = root;  
  37.             }  
  38.         }  
  39.         root = HeapRemoveTop(heap, size, distance, reflection);  
  40.         base = distance[root];  
  41.    
  42.         if (distance[root] == INFINITY) {   
  43.             /* remain nodes in heap  is not accessible */  
  44.             return g->V - (size + 1); /* 返回强连通分支结点数 */     
  45.         }  
  46.     } while (size);  
  47.    
  48.     /* successfull end algorightm  */  
  49.     return g->V;   
  50. }  

╝④

再献上一个实现

 

C代码   收藏代码
  1.     /*很裸很水的最短路,练习二叉堆优化的Dijstra~ 
  2.  
  3.     之前二叉堆优化的Prim敲了好几遍前后花了不下八个小时调试还是没有调试成功, 
  4.     但是还好,熟悉了优先队列的操作。 
  5.  
  6.     十几天后的今天重新想起这个,终于调出来了堆优化的Dijstra。理解之后还是蛮简单的。 
  7.  
  8.     一些写法并不是最优的,例如heap的实现中可以减少交换元素等。但是有这个自己写的AC 
  9.     过的Dijkstra在,以后写二叉堆优化的Prim/Dijkstra和其它优先队列的题目就可以拿它对照着Debug了。 
  10.  
  11.     2011-07-24 23:00 
  12. */  
  13.   
  14.   
  15. #include <stdio.h>  
  16.   
  17. #define MAXN 1200  
  18. #define MAXM 1200000  
  19. #define INF 19930317  
  20.   
  21. struct node  
  22. {  
  23.     int d, v, p;  
  24. }heap[MAXN];  
  25. int pos[MAXN], hl;  
  26.   
  27. int e[MAXM], cost[MAXM], next[MAXM], g[MAXN], size;  
  28.   
  29. int m, n, s, t;  
  30.   
  31. void insert(int u, int v, int w)  
  32. {  
  33.     e[++size] = v;  
  34.     next[size] = g[u];  
  35.     cost[size] = w;  
  36.     g[u] = size;  
  37. }  
  38.   
  39.   
  40. void swap(int a, int b)  
  41. {  
  42.     heap[0] = heap[a];  
  43.     heap[a] = heap[b];  
  44.     heap[b] = heap[0];  
  45.     pos[heap[a].v] = a;  
  46.     pos[heap[b].v] = b;  
  47. }  
  48.   
  49. void heapfy()  
  50. {  
  51.     int i = 2;  
  52.     while (i <= hl)  
  53.     {  
  54.         if ((i < hl) && (heap[i + 1].d < heap[i].d))  
  55.             i++;  
  56.         if (heap[i].d < heap[i >> 1].d)  
  57.         {  
  58.             swap(i, i >> 1);  
  59.             i <<= 1;  
  60.         }  
  61.         else  
  62.             break;  
  63.     }  
  64. }  
  65.   
  66.   
  67.   
  68. void decrease(int i)  
  69. {  
  70.     while ((i != 1) && (heap[i].d < heap[i >> 1].d))  
  71.     {  
  72.         swap(i, i >> 1);  
  73.         i >>= 1;  
  74.     }  
  75. }  
  76.   
  77. void relax(int u ,int v, int w)  
  78. {  
  79.     if (w + heap[pos[u]].d < heap[pos[v]].d)  
  80.     {  
  81.         heap[pos[v]].p = u;  
  82.         heap[pos[v]].d = w + heap[pos[u]].d;  
  83.         decrease(pos[v]);  
  84.     }  
  85. }  
  86.   
  87. void delete_min()  
  88. {  
  89.     swap(1, hl);  
  90.     hl--;  
  91.     heapfy();  
  92. }  
  93.   
  94. void init()  
  95. {  
  96.     int u ,v ,w, i;  
  97.   
  98.     scanf("%d%d", &m, &n);  
  99.     for (i = 1; i <= m; i++)  
  100.     {  
  101.         scanf("%d%d%d", &u, &v, &w);  
  102.         insert(u, v, w);  
  103.         insert(v, u, w);  
  104.     }  
  105.     s = 1;  
  106.     t = n;  
  107. }  
  108.   
  109.   
  110.   
  111. int dijkstra()  
  112. {  
  113.     int u, p, i;  
  114.   
  115.     for (i = 1; i <= n; i++)  
  116.     {  
  117.         heap[i].v = pos[i] = i;  
  118.         heap[i].d = INF;  
  119.     }  
  120.     heap[s].p = s;  
  121.     heap[s].d = 0;  
  122.     swap(1, s);  
  123.     hl = n;  
  124.     while (hl)  
  125.     {  
  126.         u = heap[1].v;  
  127.         delete_min();  
  128.         p = g[u];  
  129.         while (p)  
  130.         {  
  131.             if (pos[e[p]] <= hl)  
  132.                 relax(u, e[p], cost[p]);  
  133.             p = next[p];  
  134.         }  
  135.   
  136.     }  
  137. }  
  138. int main()  
  139. {  
  140.     init();  
  141.     dijkstra();  
  142.     printf("%d\n", heap[pos[t]].d);  
  143.     return 0;  
  144. }  

 ╝③

菲波那契堆实现

用类似的方法,使用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算法实现

 

Cpp代码   收藏代码
  1. #include<iostream>  
  2. #include<cstdio>  
  3. using namespace std;  
  4.   
  5. #define MAX 0x3f3f3f3f  
  6. #define N 1010  
  7. int nodenum, edgenum, original; //点,边,起点  
  8. typedef struct Edge //边  
  9. {  
  10.     int u, v;  
  11.     int cost;  
  12. }Edge;  
  13. Edge edge[N];  
  14. int dis[N], pre[N];  
  15. bool Bellman_Ford()  
  16. {  
  17.     for(int i = 1; i <= nodenum; ++i) //初始化  
  18.         dis[i] = (i == original ? 0 : MAX);  
  19.     for(int i = 1; i <= nodenum - 1; ++i)  
  20.         for(int j = 1; j <= edgenum; ++j)  
  21.             if(dis[edge[j].v] > dis[edge[j].u] + edge[j].cost) //松弛(顺序一定不能反~)  
  22.             {  
  23.                 dis[edge[j].v] = dis[edge[j].u] + edge[j].cost;  
  24.                 pre[edge[j].v] = edge[j].u;  
  25.             }  
  26.             bool flag = 1; //判断是否含有负权回路  
  27.             for(int i = 1; i <= edgenum; ++i)  
  28.                 if(dis[edge[i].v] > dis[edge[i].u] + edge[i].cost)  
  29.                 {  
  30.                     flag = 0;  
  31.                     break;  
  32.                 }  
  33.                 return flag;  
  34. }  
  35.   
  36. void print_path(int root) //打印最短路的路径(反向)  
  37. {  
  38.     while(root != pre[root]) //前驱  
  39.     {  
  40.         printf("%d-->", root);  
  41.         root = pre[root];  
  42.     }  
  43.     if(root == pre[root])  
  44.         printf("%d\n", root);  
  45. }  
  46.   
  47. int main()  
  48. {  
  49.     scanf("%d%d%d", &nodenum, &edgenum, &original);  
  50.     pre[original] = original;  
  51.     for(int i = 1; i <= edgenum; ++i)  
  52.     {  
  53.         scanf("%d%d%d", &edge[i].u, &edge[i].v, &edge[i].cost);  
  54.     }  
  55.     if(Bellman_Ford())  
  56.         for(int i = 1; i <= nodenum; ++i) //每个点最短路  
  57.         {  
  58.             printf("%d\n", dis[i]);  
  59.             printf("Path:");  
  60.             print_path(i);  
  61.         }  
  62.     else  
  63.         printf("have negative circle\n");  
  64.     return 0;  
  65. }  

 ╝⑦

 

Bellman-Ford算法优化——SPFA算法

 

循环的提前跳出:在实际操作中,贝尔曼-福特算法经常会在未达到V-1次前就出解,V-1其实是最大值。于是可以在循环中设置判定,在某次循环不再进行松弛时,直接退出循环,进行负权环判定。

具体做法是用一个队列保存待松弛的点,然后对于每个出队的点依次遍历每个与他有边相邻的点(用邻接表效率较高),如果该点可以松弛并且队列中没有该点则将它加入队列中(只有进行松弛操作的点才会对它的邻接点有影响,也就是说其邻接点才需要松弛操作),如此迭代直到队列为空。

 

SPFA算法实现

 

Cpp代码   收藏代码
  1. #include <iostream>  
  2. #include <queue>  
  3. using namespace std;  
  4. const long MAXN=10000;  
  5. const long lmax=0x7FFFFFFF;  
  6. typedef struct    
  7. {  
  8.     long v;  
  9.     long next;  
  10.     long cost;  
  11. }Edge;  
  12. Edge e[MAXN];  
  13. long p[MAXN];  
  14. long Dis[MAXN];  
  15. bool vist[MAXN];  
  16. queue<long> q;  
  17. long m,n;//点,边  
  18. void init()  
  19. {  
  20.     long i;  
  21.     long eid=0;  
  22.     memset(vist,0,sizeof(vist));  
  23.     memset(p,-1,sizeof(p));  
  24.     fill(Dis,Dis+MAXN,lmax);  
  25.     while (!q.empty())  
  26.     {  
  27.         q.pop();  
  28.     }  
  29.     for (i=0;i<n;++i)  
  30.     {  
  31.         long from,to,cost;  
  32.         scanf("%ld %ld %ld",&from,&to,&cost);  
  33.         e[eid].next=p[from];  
  34.         e[eid].v=to;  
  35.         e[eid].cost=cost;  
  36.         p[from]=eid++;  
  37.         //以下适用于无向图  
  38.         swap(from,to);  
  39.         e[eid].next=p[from];  
  40.         e[eid].v=to;  
  41.         e[eid].cost=cost;  
  42.         p[from]=eid++;  
  43.     }  
  44. }  
  45. void print(long End)  
  46. {  
  47.     //若为lmax 则不可达  
  48.     printf("%ld\n",Dis[End]);      
  49. }  
  50. void SPF()  
  51. {  
  52.     init();  
  53.     long Start,End;  
  54.     scanf("%ld %ld",&Start,&End);  
  55.     Dis[Start]=0;  
  56.     vist[Start]=true;  
  57.     q.push(Start);  
  58.     while (!q.empty())  
  59.     {  
  60.         long t=q.front();  
  61.         q.pop();  
  62.         vist[t]=false;  
  63.         long j;  
  64.         for (j=p[t];j!=-1;j=e[j].next)  
  65.         {  
  66.             long w=e[j].cost;  
  67.             if (w+Dis[t]<Dis[e[j].v])  
  68.             {  
  69.                 Dis[e[j].v]=w+Dis[t];  
  70.                 if (!vist[e[j].v])  
  71.                 {  
  72.                     vist[e[j].v]=true;  
  73.                     q.push(e[j].v);  
  74.                 }  
  75.             }  
  76.         }  
  77.     }  
  78.     print(End);  
  79. }  
  80. int main()  
  81. {  
  82.     while (scanf("%ld %ld",&m,&n)!=EOF)  
  83.     {  
  84.         SPF();  
  85.     }  
  86.     return 0;  
  87.   
  88. }  

╝⑧

 

最短路径算法之Floyd-Warshall算法

 

§3 Floyd-Warshall算法

 

Floyd-Warshall算法是解决任意两点间的最短路径的算法,可以处理有向图或负权值的最短路径问题,同时也被用于计算有向图的传递闭包。算法的时间复杂度为O(n³),空间复杂度为O(n²)。

Floyd-Warshall算法的原理是动态规划

D_{i,j,k}为从ij的只以(1..k)集合中的节点为中间節点的最短路径的长度。

  1. 若最短路径经过点k,则D_{i,j,k}=D_{i,k,k-1}+D_{k,j,k-1}
  2. 若最短路径不经过点k,则D_{i,j,k}=D_{i,j,k-1}

因此,D_{i,j,k}=\mbox{min}(D_{i,k,k-1}+D_{k,j,k-1},D_{i,j,k-1})

在实际算法中,为了节约空间,可以直接在原来空间上进行迭代,这样空间可降至二维。

╝⑨

Floyd-Warshall算法实现

C代码   收藏代码
  1. for(int k =1 ;  k <= n ; k ++ ){  
  2.     for(int i =1 ; i<= n ; i++){  
  3.         for(int j =1 ;j<=n;j++){  
  4.                dist[ i ][ j ]= min( dist[ i ][ j ],dist[ i ][ k ]+dist[ k ][ j ] );        
  5.           }  
  6.      }  
  7. }  

如果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中包含边<i, j>,则c[i, j, 0] =边<i, j> 的长度;若i= j ,则c[i,j,0]=0;如果G中不包含边<i, j>,则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。

  这样,问题便具有了最优子结构性质,可以用动态规划方法来求解。

Cpp代码   收藏代码
  1. #include <iostream>  
  2.  2 using namespace std;  
  3.  3   
  4.  4 const int INF = 100000;  
  5.  5 int n=10,map[11][11],dist[11][11][11];  
  6.  6 void init(){  
  7.  7     int i,j;  
  8.  8     for(i=1;i<=n;i++)  
  9.  9         for(j=1;j<=n;j++)  
  10. 10             map[i][j]=(i==j)?0:INF;  
  11. 11     map[1][2]=2,map[1][4]=20,map[2][5]=1;  
  12. 12     map[3][1]=3,map[4][3]=8,map[4][6]=6;  
  13. 13     map[4][7]=4,map[5][3]=7,map[5][8]=3;  
  14. 14     map[6][3]=1,map[7][8]=1,map[8][6]=2;  
  15. 15     map[8][10]=2,map[9][7]=2,map[10][9]=1;  
  16. 16 }  
  17. 17 void floyd_dp(){  
  18. 18     int i,j,k;  
  19. 19     for(i=1;i<=n;i++)  
  20. 20         for(j=1;j<=n;j++)  
  21. 21             dist[i][j][0]=map[i][j];  
  22. 22     for(k=1;k<=n;k++)  
  23. 23         for(i=1;i<=n;i++)  
  24. 24             for(j=1;j<=n;j++){  
  25. 25                 dist[i][j][k]=dist[i][j][k-1];  
  26. 26                 if(dist[i][k][k-1]+dist[k][j][k-1]<dist[i][j][k])  
  27. 27                     dist[i][j][k]=dist[i][k][k-1]+dist[k][j][k-1];  
  28. 28             }  
  29. 29 }  
  30. 30 int main(){  
  31. 31     int k,u,v;  
  32. 32     init();  
  33. 33     floyd_dp();  
  34. 34     while(cin>>u>>v,u||v){  
  35. 35         for(k=0;k<=n;k++){  
  36. 36             if(dist[u][v][k]==INF) cout<<"+∞"<<endl;  
  37. 37             else cout<<dist[u][v][k]<<endl;  
  38. 38         }  
  39. 39     }  
  40. 40     return 0;  
  41. 41 }   

╝⑨

 

最短路径算法之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操作(重赋值):边<u,v>的权值w(u,v),修改成w(u,v)+h(u)-h(v)。

4.最后,移去q,针对新图(重赋值之后的图)使用Dijkstra算法计算从每一个点s到其余另外点的最短距离。

╝⑩

Johnson算法实现:

 

Cpp代码   收藏代码
  1. #include "stdafx.h"  
  2. #include<iostream>  
  3. #define Infinity 65535  
  4. #define MAX 100  
  5. using namespace std;  
  6. //边尾节点结构体  
  7. struct edgeNode  
  8. {  
  9. int no;//节点序号  
  10. int weight; //此边权值  
  11. edgeNode *next; //下一条邻接边  
  12. };  
  13. //节点信息  
  14. struct vexNode  
  15. {  
  16. char info; //节点序号  
  17. edgeNode *link; //与此节点为首节点的边的尾节点链表  
  18. };  
  19. //优先队列元素结构体  
  20. struct PriQue  
  21. {  
  22. int no; //节点元素序号  
  23. int weight; //源点到此节点的权值  
  24. };  
  25. //节点数组  
  26. vexNode adjlist[MAX];  
  27. //添加一个序号为0节点到其他各节点的最小权值  
  28. int d[MAX];  
  29. //源点到各节点的最小权值  
  30. int lowcost[MAX];  
  31. //各节点对间的最小权值  
  32. int mincost[MAX][MAX];  
  33. //优先队列  
  34. PriQue queue[2*MAX];  
  35. //建立图的邻接表  
  36. void createGraph(vexNode *adjlist,int n,int e)  
  37. {  
  38. int i;  
  39. cout<<"请输入这些节点的信息:"<<endl;  
  40. for(i=1;i<=n;i++)  
  41. {  
  42. cout<<"节点"<<i<<"的名称:";  
  43. cin>>adjlist[i].info;  
  44. adjlist[i].link = NULL;  
  45. }  
  46. cout<<"请输入这些边的信息:"<<endl;  
  47. int v1,v2;  
  48. edgeNode *p1;  
  49. int weight1;  
  50. for(i=1;i<=e;i++)  
  51. {  
  52. cout<<"边"<<i<<"的首尾节点:";  
  53. cin>>v1>>v2;  
  54. cout<<"请输入此边的权值:";  
  55. cin>>weight1;  
  56. p1 = (edgeNode*)malloc(sizeof(edgeNode));  
  57. p1->no = v2;  
  58. p1->weight = weight1;  
  59. p1->next = adjlist[v1].link;  
  60. adjlist[v1].link = p1;  
  61. }  
  62. //添加节点0,到每一个节点的距离都是0  
  63. adjlist[0].info ='0';  
  64. adjlist[0].link = NULL;  
  65. for(i=n;i>=1;i--)  
  66. {  
  67. d[i] = 0;  
  68. p1 = (edgeNode*)malloc(sizeof(edgeNode));  
  69. p1->no = i;  
  70. p1->weight = 0;  
  71. p1->next = adjlist[0].link;  
  72. adjlist[0].link = p1;  
  73. }  
  74. }  
  75. //bellman_ford算法求节点0到其他各节点的最短距离  
  76. bool bellman_ford(vexNode *adjlist,int *d,int n)  
  77. {  
  78. int i,j;  
  79. d[0] = 0;  
  80. edgeNode *p1;  
  81. for(j=1;j<=n;j++)  
  82. {  
  83. for(i=0;i<=n;i++)  
  84. {  
  85. p1= adjlist[i].link;  
  86. while(p1 != NULL)  
  87. {  
  88. if(d[p1->no]>d[i]+p1->weight)  
  89. d[p1->no] = d[i] + p1->weight;  
  90. p1 = p1->next;  
  91. }  
  92. }  
  93. }  
  94. for(i=0;i<=n;i++)  
  95. {  
  96. p1= adjlist[i].link;  
  97. while(p1 != NULL)  
  98. {  
  99. if(d[p1->no]>d[i]+p1->weight)  
  100. return false;  
  101. p1 = p1->next;  
  102. }  
  103. }  
  104. return true;  
  105. }  
  106. //johnson算法中,需要对每一条边重新赋权值产生非负的权  
  107. void G_w_to_G1_w1(int *d,const int n)  
  108. {  
  109. int i;  
  110. edgeNode *p1;  
  111. for(i=0;i<=n;i++)  
  112. {  
  113. p1= adjlist[i].link;  
  114. while(p1 != NULL)  
  115. {  
  116. p1->weight = p1->weight + d[i] - d[p1->no];  
  117. p1 = p1->next;  
  118. }  
  119. }  
  120. }  
  121. //保持优先队列的优先性,以指定源点到每一点的最少距离为关键字  
  122. void keep_heap(PriQue *queue,int &num,int i)  
  123. {  
  124. int smallest = i;  
  125. int left = 2*i,right = 2*i+1;  
  126. if(left<=num&&queue[left].weight<queue[i].weight)  
  127. smallest = left;  
  128. if(right<=num&&queue[right].weight<queue[smallest].weight)  
  129. smallest = right;  
  130. if(smallest != i)  
  131. {  
  132. PriQue q = queue[smallest];  
  133. queue[smallest] = queue[i];  
  134. queue[i] = q;  
  135. keep_heap(queue,num,smallest);  
  136. }  
  137. }  
  138. //插入一个元素到优先队列中,并保持队列优先性  
  139. void insert_heap(PriQue *queue,int &num,int no,int wei)  
  140. {  
  141. num += 1;  
  142. queue[num].no = no;  
  143. queue[num].weight = wei;  
  144. int i = num;  
  145. while(i>1&&queue[i].weight<queue[i/2].weight)  
  146. {  
  147. PriQue q1;  
  148. q1 = queue[i/2];  
  149. queue[i/2] = queue[i];  
  150. queue[i] = q1;  
  151. i = i/2;  
  152. }  
  153. }  
  154. //取出队列首元素  
  155. PriQue heap_extract_min(PriQue *queue,int &num)  
  156. {  
  157. if(num<1)  
  158. return queue[0];  
  159. PriQue que = queue[1];  
  160. queue[1] = queue[num];  
  161. num = num -1;  
  162. keep_heap(queue,num,1);  
  163. return que;  
  164. }  
  165. //dijkstra算法求节点i到其他每一个节点的最短距离  
  166. void dijkstra(vexNode *adjlist,PriQue * queue,int i,const int n,int &num)  
  167. {  
  168. int v = i;  
  169. //lowcost[v] = 0;  
  170. int j;  
  171. for(j=1;j<n;j++)  
  172. {  
  173. edgeNode *p1 = adjlist[v].link;  
  174. while(p1 != NULL)  
  175. {  
  176. if(lowcost[p1->no] > lowcost[v] + p1->weight)  
  177. {  
  178. lowcost[p1->no] = lowcost[v] + p1->weight;  
  179. insert_heap(queue,num,p1->no,lowcost[p1->no]);  
  180. }  
  181. p1 = p1->next;  
  182. }  
  183. v = heap_extract_min(queue,num).no;  
  184. if(v==0)  
  185. {  
  186. cout<<"队列中没有节点!"<<endl;  
  187. return;  
  188. }  
  189. }  
  190. }  
  191.   
  192. int _tmain(int argc, _TCHAR* argv[])  
  193. {  
  194. int cases;  
  195. cout<<"请输入案例的个数:";  
  196. cin>>cases;  
  197. //用队列0元素作为哨兵,如果队列中没有元素,则返回队列0元素  
  198. queue[0].no = 0;  
  199. queue[0].weight = 0;  
  200. while(cases--)  
  201. {  
  202. int n,e;  
  203. cout<<"请输入节点数:";  
  204. cin>>n;  
  205. cout<<"请输入边数:";  
  206. cin>>e;  
  207. //队列中的元素,初始为0  
  208. int num = 0;  
  209. int i,j;  
  210. //创建邻接表  
  211. createGraph(adjlist,n,e);  
  212. cout<<endl;  
  213. memset(d,Infinity,sizeof(d));  
  214. //bellman_ford算法求节点0到其他各节点的最短距离  
  215. bool flag = bellman_ford(adjlist,d,n);  
  216. if(!flag)  
  217. {  
  218. cout<<"此图存在负回路,不正确!"<<endl;  
  219. continue;  
  220. }  
  221. //johnson算法中,需要对每一条边重新赋权值产生非负的权  
  222. G_w_to_G1_w1(d,n);  
  223. //运用dijkstra算法求得每一对节点间的最短距离  
  224. for(i=1;i<=n;i++)  
  225. {  
  226. for(j=1;j<=n;j++)  
  227. lowcost[j] = Infinity;  
  228. lowcost[i] =0;  
  229. dijkstra(adjlist,queue,i,n,num);  
  230. //重新把原值赋值回来,因为在函数G_w_to_G1_w1()中改变过  
  231. for(j=1;j<=n;j++)  
  232. mincost[i][j] = lowcost[j] + d[j] - d[i];  
  233. }  
  234. cout<<"下面输出每一对顶点之间的最短距离:"<<endl;  
  235. for(i=1;i<=n;i++)  
  236. for(j=1;j<=n;j++)  
  237. {  
  238. cout<<"顶点("<<i<<":"<<adjlist[i].info<<")到顶点("<<j<<":"<<adjlist[j].info<<")的最短距离为:"<<mincost[i][j]<<endl;  
  239. }  
  240. }  
  241. system("pause");  
  242. return 0;  
  243. }  

 

 

╝⑩+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://blog.csdn.net/xu3737284/article/details/8973615

给定图G(V, E)(其中V、E分别为图G的顶点集与边集),源点s,

  • 数组Distant[i]记录从源点s到顶点i的路径长度,初始化数组Distant[n]为, Distant[s]为0;
  •  
    以下操作循环执行至多n-1次,n为顶点数:
    对于每一条边e(u, v),如果Distant[u] + w(u, v) < Distant[v],则另Distant[v] = Distant[u]+w(u, v)。w(u, v)为边e(u,v)的权值;
    若上述操作没有对Distant进行更新,说明最短路径已经查找完毕,或者部分点不可达,跳出循环。否则执行下次循环;
  • 为了检测图中是否存在负环路,即权值之和小于0的环路。对于每一条边e(u, v),如果存在Distant[u] + w(u, v) < Distant[v]的边,则图中存在负环路,即是说改图无法求出单源最短路径。否则数组Distant[n]中记录的就是源点s到各顶点的最短路径长度。

可知,Bellman-Ford算法寻找单源最短路径的时间复杂度为O(V*E).

首先介绍一下松弛计算。如下图:


 

松弛计算之前,点B的值是8,但是点A的值加上边上的权重2,得到5,比点B的值(8)小,所以,点B的值减小为5。这个过程的意义是,找到了一条通向B点更短的路线,且该路线是先经过点A,然后通过权重为2的边,到达点B。
当然,如果出现一下情况


 

则不会修改点B的值,因为3+4>6。
 
Bellman-Ford算法可以大致分为三个部分
第一,初始化所有点。每一个点保存一个值,表示从原点到达这个点的距离,将原点的值设为0,其它的点的值设为无穷大(表示不可达)。
第二,进行循环,循环下标为从1到n-1(n等于图中点的个数)。在循环内部,遍历所有的边,进行松弛计算。
第三,遍历途中所有的边(edge(u,v)),判断是否存在这样情况:
d(v) > d (u) + w(u,v)
则返回false,表示途中存在从源点可达的权为负的回路。
 
之所以需要第三部分的原因,是因为,如果存在从源点可达的权为负的回路。则 应为无法收敛而导致不能求出最短路径。
考虑如下的图:
 

经过第一次遍历后,点B的值变为5,点C的值变为8,这时,注意权重为-10的边,这条边的存在,导致点A的值变为-2。(8+ -10=-2)
 
 

第二次遍历后,点B的值变为3,点C变为6,点A变为-4。正是因为有一条负边在回路中,导致每次遍历后,各个点的值不断变小。
 
在回过来看一下bellman-ford算法的第三部分,遍历所有边,检查是否存在d(v) > d (u) + w(u,v)。因为第二部分循环的次数是定长的,所以如果存在无法收敛的情况,则肯定能够在第三部分中检查出来。比如
 

此时,点A的值为-2,点B的值为5,边AB的权重为5,5 > -2 + 5. 检查出来这条边没有收敛。
 
所以,Bellman-Ford算法可以解决图中有权为负数的边的单源最短路径问。

个人感觉算法导论讲解很不错,把这一章贴出来和大家分享:

24.1 The Bellman-Ford algorithm

The Bellman-Ford algorithm solves the single-source shortest-paths problem in the general case in which edge weights may be negative. Given a weighted, directed graph G = (VE) with source s and weight function w : E → R, the Bellman-Ford algorithm returns a boolean value indicating whether or not there is a negative-weight cycle that is reachable from the source. If there is such a cycle, the algorithm indicates that no solution exists. If there is no such cycle, the algorithm produces the shortest paths and their weights.

The algorithm uses relaxation, progressively decreasing an estimate d[v] on the weight of a shortest path from the source s to each vertex v ∈ V until it achieves the actual shortest-path weight δ(sv). The algorithm returns TRUE if and only if the graph contains no negative-weight cycles that are reachable from the source.

BELLMAN-FORD(G, w, s)
1  INITIALIZE-SINGLE-SOURCE(G, s)
2  for i1 to |V[G]| - 1
3       do for each edge (u, v) ∈ E[G]
4              do RELAX(u, v, w)
5  for each edge (u, v) ∈ E[G]
6       do if d[v] > d[u] + w(u, v)
7             then return FALSE
8  return TRUE

Figure 24.4 shows the execution of the Bellman-Ford algorithm on a graph with 5 vertices. After initializing the d and π values of all vertices in line 1, the algorithm makes |V| – 1 passes over the edges of the graph. Each pass is one iteration of the for loop of lines 2-4 and consists of relaxing each edge of the graph once. Figures 24.4(b)-(e) show the state of the algorithm after each of the four passes over the edges. After making |V|- 1 passes, lines 5-8 check for a negative-weight cycle and return the appropriate boolean value. (We’ll see a little later why this check works.)

(单击图片可以放大)

Figure 24.4: The execution of the Bellman-Ford algorithm. The source is vertex s. The d values are shown within the vertices, and shaded edges indicate predecessor values: if edge (u, v) is shaded, then π[v] = u. In this particular example, each pass relaxes the edges in the order (t, x), (t, y), (t, z), (x, t), (y, x), (y, z), (z, x), (z, s), (s, t), (s, y). (a) The situation just before the first pass over the edges. (b)-(e) The situation after each successive pass over the edges. The d and π values in part (e) are the final values. The Bellman-Ford algorithm returns TRUE in this example.

The Bellman-Ford algorithm runs in time O(V E), since the initialization in line 1 takes Θ(V) time, each of the |V| – 1 passes over the edges in lines 2-4 takes Θ(E) time, and the for loop of lines 5-7 takes O(E) time.

以下是Bellman-Ford代码:

[cpp]  view plain copy
  1. #include <iostream>  
  2. using namespace std;  
  3. const int maxnum = 100;  
  4. const int maxint = 99999;  
  5.   
  6. // 边,  
  7. typedef struct Edge{  
  8.     int u, v;    // 起点,重点  
  9.     int weight;  // 边的权值  
  10. }Edge;  
  11.   
  12. Edge edge[maxnum];     // 保存边的值  
  13. int  dist[maxnum];     // 结点到源点最小距离  
  14.   
  15. int nodenum, edgenum, source;    // 结点数,边数,源点  
  16.   
  17. // 初始化图  
  18. void init()  
  19. {  
  20.     // 输入结点数,边数,源点  
  21.     cin >> nodenum >> edgenum >> source;  
  22.     for(int i=1; i<=nodenum; ++i)  
  23.         dist[i] = maxint;  
  24.     dist[source] = 0;  
  25.     for(int i=1; i<=edgenum; ++i)  
  26.     {  
  27.         cin >> edge[i].u >> edge[i].v >> edge[i].weight;  
  28.         if(edge[i].u == source)          //注意这里设置初始情况  
  29.             dist[edge[i].v] = edge[i].weight;  
  30.     }  
  31. }  
  32.   
  33. // 松弛计算  
  34. void relax(int u, int v, int weight)  
  35. {  
  36.     if(dist[v] > dist[u] + weight)  
  37.         dist[v] = dist[u] + weight;  
  38. }  
  39.   
  40. bool Bellman_Ford()  
  41. {  
  42.     for(int i=1; i<=nodenum-1; ++i)  
  43.         for(int j=1; j<=edgenum; ++j)  
  44.             relax(edge[j].u, edge[j].v, edge[j].weight);  
  45.     bool flag = 1;  
  46.     // 判断是否有负环路  
  47.     for(int i=1; i<=edgenum; ++i)  
  48.         if(dist[edge[i].v] > dist[edge[i].u] + edge[i].weight)  
  49.         {  
  50.             flag = 0;  
  51.             break;  
  52.         }  
  53.     return flag;  
  54. }  
  55. int main()  
  56. {  
  57.     //freopen("input3.txt", "r", stdin);  
  58.     init();  
  59.     if(Bellman_Ford())  
  60.         for(int i = 1 ;i <= nodenum; i++)  
  61.             cout << dist[i] << endl;  
  62.     return 0;  
  63. }  


补充:


考虑:为什么要循环V-1次?
答:因为最短路径肯定是个简单路径,不可能包含回路的,
如果包含回路,且回路的权值和为正的,那么去掉这个回路,可以得到更短的路径
如果回路的权值是负的,那么肯定没有解了

图有n个点,又不能有回路
所以最短路径最多n-1边

又因为每次循环,至少relax一边
所以最多n-1次就行了


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值