最短路径算法

 在准备ACM比赛的过程中,研究了图论中一些算法。首先研究的便是最短路的问题。《离散数学》第四版(清华大学出版社)一书中讲解的Dijkstra算法是我首先研究的源材料。

      如何求图中V0到V5的最短路径呢?

最短路径算法_第1张图片

        java实现的方式如下: 

       第一步,根据图来建立权值矩阵:

       int[][] W = { 
    {  0,   1,   4,  -1,  -1,  -1 },
    {  1,   0,   2,   7,    5,  -1 },
    {  4,   2,   0,  -1,    1,  -1 }, 
    { -1,  7,  -1,   0,    3,    2 },
    { -1,  5,    1,   3,   0,    6 }, 
    { -1, -1,  -1,   2,   6,    0 } };(-1表示两边不相邻,权值无限大)

例如:W[0][2]=4 表示点V0到点V2的权值为4

W[0][3]=-1表示点V0与V3不相邻,所以权值无限大。

第二步:对V0标号;V0到其它点的路径得到 distance: {0,1,4,-1,-1,-1}; 找到V0到各点中权值最小的那个点(标号的点除外,-1代表无限大),故得到1即对应的下标1,得到V1;对V1标号,然后更改V0通过V1到其它点的路径得到 distance: { 0, 1, 3, 8, 6, -1}; 

第三步:找到distance中权值最小的那个点,(标号的点除外)得到V2,对V2标号,然后更改V0通过V1->V2到其它点的路径得到 distance: { 0, 1, 3, 8, 4, -1}; 

第四步:找到distance中权值最小的那个点,(标号的点除外)得到V4,对V4标号,然后更改V0通过V1->V2到其它点的路径得到 distance: { 0, 1, 3, 7, 4, 10}; 

第四步:找到distance中权值最小的那个点,(标号的点除外)得到V3,对V3标号,然后更改V0通过V1->V2到其它点的路径得到 distance: { 0, 1, 3, 7, 4, 9}; 

最后只剩下V5没有被标号,就找到V5了。结束!

源代码如下:

   
   
   
   
  1. package com.xh.Dijkstra;  
  2.  
  3. //这个算法用来解决无向图中任意两点的最短路径  
  4. public class ShortestDistanceOfTwoPoint_V5 {  
  5.     public static int dijkstra(int[][] W1, int start, int end) {  
  6.         boolean[] isLabel = new boolean[W1[0].length];// 是否标号  
  7.         int[] indexs = new int[W1[0].length];// 所有标号的点的下标集合,以标号的先后顺序进行存储,实际上是一个以数组表示的栈  
  8.         int i_count = -1;//栈的顶点  
  9.         int[] distance = W1[start].clone();// v0到各点的最短距离的初始值  
  10.         int index = start;// 从初始点开始  
  11.         int presentShortest = 0;//当前临时最短距离  
  12.  
  13.         indexs[++i_count] = index;// 把已经标号的下标存入下标集中  
  14.         isLabel[index] = true;  
  15.           
  16.         while (i_count0].length) {  
  17.             // 第一步:标号v0,即w[0][0]找到距离v0最近的点  
  18.  
  19.             int min = Integer.MAX_VALUE;  
  20.             for (int i = 0; i < distance.length; i++) {  
  21.                 if (!isLabel[i] && distance[i] != -1 && i != index) {  
  22.                     // 如果到这个点有边,并且没有被标号  
  23.                     if (distance[i] < min) {  
  24.                         min = distance[i];  
  25.                         index = i;// 把下标改为当前下标  
  26.                     }  
  27.                 }  
  28.             }  
  29.             if (index == end) {//已经找到当前点了,就结束程序  
  30.                 break;  
  31.             }  
  32.             isLabel[index] = true;//对点进行标号  
  33.             indexs[++i_count] = index;// 把已经标号的下标存入下标集中  
  34.             if (W1[indexs[i_count - 1]][index] == -1 
  35.                     || presentShortest + W1[indexs[i_count - 1]][index] > distance[index]) {  
  36.                 // 如果两个点没有直接相连,或者两个点的路径大于最短路径  
  37.                 presentShortest = distance[index];  
  38.             } else {  
  39.                 presentShortest += W1[indexs[i_count - 1]][index];  
  40.             }  
  41.  
  42.             // 第二步:将distance中的距离加入vi  
  43.             for (int i = 0; i < distance.length; i++) {  
  44.                 // 如果vi到那个点有边,则v0到后面点的距离加  
  45.                 if (distance[i] == -1 && W1[index][i] != -1) {// 如果以前不可达,则现在可达了  
  46.                     distance[i] = presentShortest + W1[index][i];  
  47.                 } else if (W1[index][i] != -1 
  48.                         && presentShortest + W1[index][i] < distance[i]) {  
  49.                     // 如果以前可达,但现在的路径比以前更短,则更换成更短的路径  
  50.                     distance[i] = presentShortest + W1[index][i];  
  51.                 }  
  52.  
  53.             }  
  54.         }  
  55.         //如果全部点都遍历完,则distance中存储的是开始点到各个点的最短路径  
  56.         return distance[end] - distance[start];  
  57.     }  
  58.     public static void main(String[] args) {  
  59.         // 建立一个权值矩阵  
  60.         int[][] W1 = { //测试数据1  
  61.                 { 014, -1, -1, -1 },  
  62.                 { 10275, -1 },  
  63.                 { 420, -11, -1 },   
  64.                 { -17, -1032 },  
  65.                 { -151306 },   
  66.                 { -1, -1, -1260 } };  
  67.         int[][] W = { //测试数据2  
  68.                 { 0134 },  
  69.                 { 102, -1 },  
  70.                 { 3205 },  
  71.                 { 4, -150 } };  
  72.  
  73.         System.out.println(dijkstra(W1, 0,4));  
  74.  
  75.     }  
  76. }  

如果需要求无向图各个点的最短距离矩阵,则多次运用dijkstra算法就可以了,代码如下:

   
   
   
   
  1. package com.xh.Dijkstra;  
  2.  
  3. //这个程序用来求得一个图的最短路径矩阵  
  4. public class ShortestDistance_V4 {  
  5.     public static int dijkstra(int[][] W1, int start, int end) {  
  6.         boolean[] isLabel = new boolean[W1[0].length];// 是否标号  
  7.         int min = Integer.MAX_VALUE;  
  8.         int[] indexs = new int[W1[0].length];// 所有标号的点的下标集合  
  9.         int i_count = -1;  
  10.         int index = start;// 从初始点开始  
  11.         int presentShortest = 0;  
  12.         int[] distance = W1[start].clone();// v0到各点的最短距离的初始值  
  13.         indexs[++i_count] = index;// 把已经标号的下标存入下标集中  
  14.         isLabel[index] = true;  
  15.         while (true) {  
  16.             // 第一步:标号v0,即w[0][0]找到距离v0最近的点  
  17.  
  18.             min = Integer.MAX_VALUE;  
  19.             for (int i = 0; i < distance.length; i++) {  
  20.                 if (!isLabel[i] && distance[i] != -1 && i != index) {  
  21.                     // 如果到这个点有边,并且没有被标号  
  22.                     if (distance[i] < min) {  
  23.                         min = distance[i];  
  24.                         index = i;// 把下标改为当前下标  
  25.                     }  
  26.                 }  
  27.             }  
  28.             if (index == end) {  
  29.                 break;  
  30.             }  
  31.             isLabel[index] = true;  
  32.             indexs[++i_count] = index;// 把已经标号的下标存入下标集中  
  33.             if (W1[indexs[i_count - 1]][index] == -1 
  34.                     || presentShortest + W1[indexs[i_count - 1]][index] > distance[index]) {  
  35.                 presentShortest = distance[index];  
  36.             } else {  
  37.                 presentShortest += W1[indexs[i_count - 1]][index];  
  38.             }  
  39.  
  40.             // 第二步:奖distance中的距离加入vi  
  41.             for (int i = 0; i < distance.length; i++) {  
  42.                 // 如果vi到那个点有边,则v0到后面点的距离加  
  43.                 // 程序到这里是有问题滴! 呵呵  
  44.                 if (distance[i] == -1 && W1[index][i] != -1) {// 如果以前不可达,则现在可达了  
  45.                     distance[i] = presentShortest + W1[index][i];  
  46.                 } else if (W1[index][i] != -1 
  47.                         && presentShortest + W1[index][i] < distance[i]) {  
  48.                     // 如果以前可达,但现在的路径比以前更短,则更换成更短的路径  
  49.                     distance[i] = presentShortest + W1[index][i];  
  50.                 }  
  51.  
  52.             }  
  53.         }  
  54.         return distance[end] - distance[start];  
  55.     }  
  56.    
  57.     public static int[][] getShortestPathMatrix(int[][] W) {  
  58.         int[][] SPM = new int[W.length][W.length];  
  59.         //多次利用dijkstra算法  
  60.         for (int i = 0; i < W.length; i++) {  
  61.             for (int j = i + 1; j < W.length; j++) {  
  62.                 SPM[i][j] =dijkstra(W, i, j);  
  63.                 SPM[j][i] = SPM[i][j];  
  64.             }  
  65.         }  
  66.         return SPM;  
  67.     }  
  68.  
  69.     public static void main(String[] args) {  
  70.         /* 顶点集:V={v1,v2,……,vn} */ 
  71.         int[][] W = { { 0134 }, { 102, -1 }, { 3205 },  
  72.                 { 4, -150 } };  
  73.         int[][] W1 = { { 014, -1, -1, -1 }, { 10275, -1 },  
  74.                 { 420, -11, -1 }, { -17, -1032 },  
  75.                 { -151306 }, { -1, -1, -1260 } };// 建立一个权值矩阵  
  76.         ;// 建立一个权值矩阵  
  77.         int[][] D = getShortestPathMatrix(W1);  
  78.         //输出最后的结果  
  79.         for (int i = 0; i < D.length; i++) {  
  80.             for (int j = 0; j < D[i].length; j++) {  
  81.                 System.out.print(D[i][j] + " ");  
  82.             }  
  83.             System.out.println();  
  84.         }  
  85.     }  
  86. }  




最短路径问题—Bellman-Ford算法

一、算法思想

1Dijkstra算法的局限性

上节介绍了Dijkstra算法,该算法要求网络中各边上得权值大于或等于0.如果有向图中存在带负权值的边,则采用Dijkstra算法求解最短路径得到的结果有可能是错误的。

例如,对下图所示的有向图,采用Dijkstra算法求得顶点v0到顶点v2的最短距离是dist[2],即v0到v2的直接路径,长度为5.但从v0到v2的最短路径应该是(v0,v1,v2),其长度为2。

 最短路径算法_第2张图片

 


如果把图1(a)中的边<1,2>的权值由-5改成5,则采用Dijkstra算法求解最短路径,得到的结果是正确的(这里不再求解)。

为什么当有向图中存在带负权值的边时,采用Dijkstra算法求解得到的最短路径有时是错误的?答案是:Dijkstra算法在利用顶点u的dist[]去递推T集合各顶点的dist[k]值时,前提是顶点u的dist[]值时当前T集合中最短路径长度最小的。如果图中所有边的权值都是正的,这样推导是没有问题的。但是如果有负权值的边,这样推导是不正确的。例如,在图1(d)中,第1次在T集合中找到dist[]最小的是顶点2,dist[2]等于5;但是顶点0距离顶点2的最短路径是(v0,v1,v2),长度为2,而不是5,其中边<1,2>是一条负权值边。

 

2Bellman-Ford算法思想

为了能够求解边上带有负权值的单源最短路径问题,Bellman(贝尔曼)和Ford(福特)提出了从源点逐次途经其他顶点,以缩短到达终点的最短路径长度的方法。该方法也有一个限制条件:要求图中不能包含权值总和为负值的回路。

例如图2(a)所示的有向图中,回路(v0,v1,v0)包括了一条具有负权值的边,且其路径长度为-1。当选择的路径为(v0,v1,v0,v1,v0,v1,v0,v1,…)时,路径的长度会越来越小,这样顶点0到顶点2的路径长度最短可达-∞。如果存在这样的回路,则不能采用Bellman-Ford算法求解最短路径。

如果有向图中存在由带负权值的边组成的回路,但回路权值总和非负,则不影响Bellman-Ford算法的求解,如图2(b)所示。

 最短路径算法_第3张图片

 

 

权值总和为负值的回路我们称为负权值回路,在Bellman-Ford算法中判断有向图中是否存在负权值回路的方法,见后面。

假设有向图中有n个不存在负权值回路,从顶点v1和到顶点v25如果存在最短路径,则此路径最多有n-1条边。这是因为如果路径上得边数超过了n-1条时,必然会重复经过一个顶点,形成回路;而如果这个回路的权值总和为非负时,完全可以去掉这个回路,使得v1到v2的最短路径长度缩短。下面将以此为依据,计算从源点v0到其他每个顶点u的最短路径长度dist[u]。

Bellman-Ford算法构造一个最短路径长度数组序列:dist1[u],dist2[u],dist3[u],…,distn-1[u]。其中:

dist1[u]为从源点v0到终点u的只经过一条边的最短路径的长度,并有dist1[u]=edge[v0,u]。

dist2[u]为从源点v0出发最多经过不构成负权值回路的两条边到达终点u的最短路径长度。

dist3[u]为从源点v0出发最多经过不构成负权值回路的3条边到达终点u的最短路径长度。

……

distn-1[u]为从源点v0出发最多经过不构成负权值回路的n-1条边到达终点u的最短路径长度。

算法的最终目的是计算出distn-1[u],为源点v0到顶点u的最短路径长度。

采用递推方式计算distk[u]。

设已经求出distk-1[u],u=0,1,…,n-1,此即从源点v0最多经过不构成负权值回路的k-1条边到达终点u的最短路径的长度。

从图的邻接矩阵可以找出各个顶点j到达顶点u的(直接边)距离edge[j,u],计算min{distk-1[j]+edge[j,u]},可得从源点v0途经各个顶点,最多经过不构成回路的k条边到达终点u的最短路径的长度。

比较distk-1[u]和min{distk-1[j]+edge[j,u]},取较小者作为distk[u]的值。

因此Bellman-Ford算法的递推公式(求源点v0到各顶点u的最短路径)为:

初始:dist1[u]=edge[v0,u],v0是源点

递推:distk[u]=min{distk-1[u],min{distk-1[j]+edge[j,u]}}

     j=0,1,...,n-1,j<>u; k=2,3,4,...,n-1

 

二、算法实现

Bellman-Ford算法在实现时,需要使用以下两个数组。

(1)   使用同一个数组dist[n]来存放一系列的distk[n],其中k=1,2,...,n-1;算法结束时dist[u]数组中存放的是distn-1[u];

(2)   path[n]数组含义同Dijkstra算法中的path数组。

 

【例题1】利用Bellman-Ford算法求解下图(a)中顶点0到其他各顶点的最短路径长度,并输出对应的最短路径。

输入:首先输入顶点个数n,然后输入每条边的数据。每条边的数据格式为:u v w,分别表示这条边的起点、终点和边上的权值。顶点序号从0开始计起。最后一行为-1 -1 -1,表示输入数据的结束。

样例输入:                  样例输出:

7                                 1     0->3->2->1

0 1 6                            3     0->3->2

0 2 5                            5     0->3

0 3 5                            0     0->3->2->1->4

1 4 -1                           4     0->3->5

2 1 -2                           3     0->3->2->1->4->6

2 4 1

3 2 -2

3 5 -1

4 6 3

5 6 3

-1 -1 -1

 最短路径算法_第4张图片

【分析】

  如图3(c)所示,k=1时,dist数组各元素的值dist[u]就是edge[0,u](见图3(b))。在Bellman-Ford算法执行过程中,dist数组各元素的变化如图3(c)所示。在图3(c)中,dist[u]的值如有更新,则用粗体,红色标明,u=1,2,3,4,5,6。以k=2,u=1加以解释。求dist2[1]的递推公式是:

dist2[1]=min{ dist1[1],min{dist1[j]+edge[j,1]}}   j=0,2,3,4,5,6

所以,在程序中k=2时,dist[1]的值为:

 dist[1]=min{6,min{dist[0]+edge[0,1],dist[2]+edge[2,1], dist[3]+edge[3,1], dist[4]+edge[4,1],

dist[5]+edge[5,1], dist[6]+edge[6,1]}}

     =min{6,min{0+6, 5+(-2), 5+∞, ∞+∞, ∞+∞, ∞+∞}}

     =3

此时dist[1]的值为从源点v0出发,经过不构成负权值回路的两条边到达顶点v1的最短路径长度,其路径为(v0,v2,v1)。

    在Bellman-Ford算法执行过程中,path[n]数组的变化与Dijkstra算法类似,所以在图3中没有列出path[n]数组的变化过程。当顶点0到其他各顶点的最短路径长度求解完毕后,如果根据path数组求解顶点0到其他各顶点vk的最短路径?方法跟Dijkstra算法中的方法完全一样:从path[k]开始,采用“倒向追踪”方法,一直找到源点v0。

在下面的代码中,bellman(v0)函数实现了求源点v0到其他各顶点的最短路径。在主函数中调用bellman(0),则求解的是从顶点0到其他各顶点的最短路径。另外,主函数中的shortest数组用来保存最短路径上各个顶点的序号,其作用和上一讲中Dijkstra程序代码中的shortest数组的作用一样。

【参考程序】

var n:longint; //顶点个数
    i,j,k,u,v,w:longint;
    edge:array[0..8,0..8]of longint; //邻接矩阵,这里的8为顶点个数最大值,可修改
    dist,path,shortest:array[0..8]of longint; //shortest数组是输出最短路径上的各个顶点时存放各个顶点的序号
procedure bellman(v0:longint);  //求顶点v0到其他顶点的最短路径
  var i,j,k,u:longint;
  begin
    for i:=0 to n-1 do //初始化
      begin
        dist[i]:=edge[v0,i];
        if (i<>v0)and(dist[i]<1000000) then path[i]:=v0 else path[i]:=-1;
      end;
    for k:=2 to n-1 do //从dist(1)[u]递推出dist(2)[u],...,dist(n-1)[u]
      begin
        for u:=0 to n-1 do //修改每个顶点的dist[u]和path[u]
          if u<>v0 then
              for j:=0 to n-1 do //考虑其他每个顶点
                if (edge[j,u]<1000000)and(dist[j]+edge[j,u]0 do
        begin inc(k);shortest[k]:=path[shortest[k-1]];end;
      inc(k);shortest[k]:=0;
      for j:=k downto 1 do write(shortest[j],'->');
      writeln(shortest[0]);
   end;
end.



 

三、关于Bellman-Ford算法的进一步讨论

1)本质思想

    在从dist(k-1)[]递推到dist(k)[]的过程中,Bellman-Ford算法的本质是对每条边进行判断:设边的权值为w(u,v),如图4所示,如果边的引入会使得dist(k-1)[v]的值再减小,则要修改dist(k-1)[v],即:如果dist(k-1)[u]+w(u,v)松弛(slack)。

 

按照这样的思想,Bellman-Ford算法的递推公式应该改为(求源点v0到各顶点v的最短路径):

初始:dist(0)[v]=∞,dist(0)[v0]=0,v0是源点,v<>v0

递推:对每条边(u,v),dist(k)[v]=min{dist(k-1)[v],dist(k-1)[u]+w(u,v)}  k=1,2,3,...,n-1

理解了这点,就能理解Bellman-Ford算法的复杂度分析、Bellman-Ford算法的优化等。

 

2)时间复杂度分析

在例题1的Bellman-Ford算法代码中,有一个三重嵌套的for循环,如果使用邻接矩阵存储有向图,最内层的if语句的总执行次数为n^3,所以算法的时间复杂度为o(n^3)。

如果使用邻接表来存储有向图,内层的两个for循环可以改成一个while循环,可以使算法的时间复杂度降为o(n*m),其中n为有向图中顶点个数,m为边的数目。这时因为,邻接表里直接存储了边地信息,浏览完所有的边,复杂度为o(m)。而邻接矩阵是间接存储边,浏览完所有的边,复杂度为o(n^2)。

使用邻接表存储思想实现Bellman-Ford算法的具体过程为:

将每条边的信息(两个顶点u,v和权值w,可以使用记录record来实现)存储到一个数组edges中,从dist(k-1)[]递推到dist(k)[]的过程中,对edges数组中的每条边,判断一下边的引入,是否会缩短源点v0到顶点v的最短路径长度。

根据上面的分析,可以将例题1中的bellman函数简化成如下代码:

procedure bellman(v0:longint);  //求顶点v0到其他顶点的最短路径
  vari,k:longint;
 begin
   for i:=0 to n-1 do //初始化dist数组
     begin dist[i]:=1000000;path[i]:=-1; end;
   dist[v0]:=0;
   for k:=1 to n-1 do //从dist(0)[u]递推出dist(1)[u],...,dist(n-1)[u]
     //判断第i条边的引入,是否会缩短源点v0到顶点v的最短路径长度
       for i:=0 to m-1 do //m为边的数目,即edges数组中元素个数
         if(dist[edges[i].u]<.1000000)and(edges[i].w+dist[edges[i].u]


  其中,dist数组各元素中,除源点v0外,其他顶点的dist[]值都初始化为∞,这样Bellman-Ford算法需要多递推一次,详见后面。

 

3Bellman-Ford算法负权值回路的判断方法

如果存在从源点可达的负权值回路,则最短路径不存在,因为可以重复走这个回路,使得路径无穷小。在Bellman-Ford算法中,判断是否存在从源点可达的负权值回路的方法如下。在求出dist(n-1)[]后,再对每条边判断一下:加入这条边是否会使得顶点v的最短路径值再缩短。即判断:

dist[u]+edge[u,v]

是否成立,如果成立,则说明存在从源点可达的负权值回路。代码如下:

for i:=0 to n-1 do  //采用邻接矩阵

  for j:=0 to n-1 do

    if (edge[i,j]<1000000)and(dist[i]+edge[i,j]

exit(1); //不存在从源点可达的负权值回路  

 

或者:

for i:=0 to m-1 do

  if(dist[edges[i].u]<>1000000)and(edges[i].w+dist[edges[i].u]

exit(1); //不存在从源点可达的负权值回路

 

4Bellman-Ford算法中数组dist的初始值

dist数组的初始值为邻接矩阵中源点v0所在的行,实际上还可以采用以下方式对dist数组初始化:除源点v0外,其他顶点的最短距离初始为∞(在程序实现时可以用一个权值不会达到的一个大数表示):源点dist[v0]=0。这样Bellman-Ford算法的第1重for循环要多执行一次,即要执行n-1次;且执行第1次后,dist数组的取值为邻接矩阵中源点v0所在的行。

 

5Bellman-Ford算法的改进

Bellman-Ford算法是否一定要循环n-2次,n为顶点个数,即是否一定需要从dist(1)[u]递推到dist(n-1)[u]?

答案是未必!其实只要在某次循环过程中,考虑每条边后,都没能改变当前源点到所有顶点的最短路径长度,那么Bellman-Ford算法就可以提前结束了。

 


你可能感兴趣的:(【数据结构与算法】)