最短路径(Floyd、Dijstra、BellmanFord)

1、迪杰斯特拉(Dijstra)

2、 弗洛伊德(Floyd)

3、 贝尔曼-福特算法(BellmanFord)

1、迪杰斯特拉(Dijstra)

算法描述 :
1)算法思想:设G=(V,E)是一个带权有向图,把图中顶点集合V分成两组,第一组为已求出最短路径的顶点集合(用S表示,初始时S中只有一个源点,以后每求得一条最短路径 , 就将加入到集合S中,直到全部顶点都加入到S中,算法就结束了),第二组为其余未确定最短路径的顶点集合(用U表示),按最短路径长度的递增次序依次把第二组的顶点加入S中。在加入的过程中,总保持从源点v到S中各顶点的最短路径长度不大于从源点v到U中任何顶点的最短路径长度。此外,每个顶点对应一个距离,S中的顶点的距离就是从v到此顶点的最短路径长度,U中的顶点的距离,是从v到此顶点只包括S中的顶点为中间顶点的当前最短路径长度。

迪杰斯特拉算法算法不能解决带有“负权回路”

这里写图片描述

#include <stdio.h>  
#include <stdlib.h>  
#define MAXVEX 9  
#define INFINITY 65535  

typedef int Patharc[MAXVEX];                                //存放最短路径上的结点编号  
typedef int ShortPathTable[MAXVEX];                        //各节点到源点的路径(经过中间结点)长度  

struct MGraph{  
    int numVertexes;  
    int *vex;  
    int arc[MAXVEX][MAXVEX];  
};  

void ShortestPath_Dijkstra(MGraph *G,int v0,int vv,Patharc *P,ShortPathTable *D)  

{  

    int v,w,k,min;  
    int result[MAXVEX];  
    int n=0;     

     for(v=0;v<G->numVertexes;++v)  
     {  
       result[v]=0;  
       (*D)[v]=G->arc[v0][v];                 //初始化,其余各节点到源点的距离  
       (*P)[v]=-1;  

    }  

    (*D)[v0]=0;  
    result[v0]=1;                         //源点加入  
    (*P)[n++]=v0;  


    for(v=0;v<G->numVertexes;++v)  

      {  

         min=INFINITY;  

         for(w=0;w<G->numVertexes;++w)                                 //寻找未加入的所有结点中到源点距离最短的结点k  

         {  

            if(!result[w] && (*D)[w]<min)  

             {  
                k=w;  
                min=(*D)[w];  
             }  
         }  

         result[k]=1;                         //结点k加入  

         (*P)[n++]=k;  

          //判断是否到达终点  

         if(k==vv)  
             break;     

         for(w=0;w<G->numVertexes;++w)                             //更新其余各节点到源点的距离的距离数组  
         {  
             //if(!result[w] && (min+G->arc[k][w]<(*D)[w]))                //新节点w到源点的距离=k到源点的距离+k到w的距离  

             if(!result[w])  
              {  
                 (*D)[w]=min+G->arc[k][w];  
                  //(*P)[w]=k;  
              }  

          }  

      }  

      //输出结果  

     printf("\n");  

     for(v=0;v<G->numVertexes;++v)  
     {  
         printf("%d-->",(*P)[v]);  
     }  
  }  


void main()    
{    
     MGraph *my_g=(struct MGraph*)malloc(sizeof(struct MGraph));    
     int i,j;    
     int t=0;  
     int v0=0;  
     int vv=8;  

     my_g->numVertexes=9;    
     my_g->vex=(int*)malloc(sizeof(char)*my_g->numVertexes);    
     if(!my_g->vex) return;    

     for(i=0;i<my_g->numVertexes;++i)                                 //一维数组(图中各结点)初始化{0,1,2,3,4,5,6,7,8}    
          my_g->vex[i]=i++;  

      for(i=0;i<my_g->numVertexes;++i)    
          for(j=0;j<my_g->numVertexes;++j)    
              my_g->arc[i][j]=INFINITY;  



      // 无向图的权值二维数组为对称矩阵  

      my_g->arc[0][1]=1;  my_g->arc[0][2]=5;  

      my_g->arc[1][2]=3;  my_g->arc[1][3]=7;  my_g->arc[1][4]=5;    

      my_g->arc[2][4]=1;  my_g->arc[2][5]=7;  

      my_g->arc[3][4]=2;  my_g->arc[3][6]=3;  

      my_g->arc[4][5]=3;  my_g->arc[4][6]=6;  my_g->arc[4][7]=9;  

      my_g->arc[5][7]=5;  

      my_g->arc[6][7]=2;  my_g->arc[6][8]=7;  

      my_g->arc[7][8]=4;  

     for(i=0;i<my_g->numVertexes;++i)    
          for(j=0;j<=i;++j)    
         {    
             if(i==j)    
             {    
                my_g->arc[i][j]=0;    
                continue;    
             }    

              my_g->arc[i][j]=my_g->arc[j][i];    

          }    

      for(i=0;i<my_g->numVertexes;++i)                                 //二维数组表示图中各结点间连接边的weight    
      {    
          for(j=0;j<my_g->numVertexes;++j)    
          printf("%5d  ",my_g->arc[i][j]);    
          printf("\n");    
      }    
     printf("\n\n");  


     Patharc P;  
     ShortPathTable D;  
     ShortestPath_Dijkstra(my_g,v0,vv,&P,&D);  

     free(my_g->vex);  
      //free(my_g->arc);  

  }

这里写图片描述

2、弗洛伊德(Floyd)

—-摘自《啊哈算法》

此算法由Robert W. Floyd(罗伯特·弗洛伊德)于1962年发表在“Communications of the ACM”上。同年Stephen Warshall(史蒂芬·沃舍尔)也独立发表了这个算法。Robert W.Floyd这个牛人是朵奇葩,他原本在芝加哥大学读的文学,但是因为当时美国经济不太景气,找工作比较困难,无奈之下到西屋电气公司当了一名计算机操作员,在IBM650机房值夜班,并由此开始了他的计算机生涯。
这里写图片描述

暑假,小哼准备去一些城市旅游。有些城市之间有公路,有些城市之间则没有,如下图。为了节省经费以及方便计划旅程,小哼希望在出发之前知道任意两个城市之前的最短路程。
这里写图片描述

上图中有4个城市8条公路,公路上的数字表示这条公路的长短。请注意这些公路是单向的。我们现在需要求任意两个城市之间的最短路程,也就是求任意两个点之间的最短路径。这个问题这也被称为“多源最短路径”问题。

现在需要一个数据结构来存储图的信息,我们仍然可以用一个4*4的矩阵(二维数组e)来存储。比如1号城市到2号城市的路程为2,则设e[1][2]的值为2。2号城市无法到达4号城市,则设置e[2][4]的值为∞。另外此处约定一个城市自己是到自己的也是0,例如e[1][1]为0,具体如下。

这里写图片描述

现在回到问题:如何求任意两点之间最短路径呢?通过之前的学习我们知道通过深度或广度优先搜索可以求出两点之间的最短路径。所以进行n2遍深度或广度优先搜索,即对每两个点都进行一次深度或广度优先搜索,便可以求得任意两点之间的最短路径。可是还有没有别的方法呢?

我们来想一想,根据我们以往的经验,如果要让任意两点(例如从顶点a点到顶点b)之间的路程变短,只能引入第三个点(顶点k),并通过这个顶点k中转即a->k->b,才可能缩短原来从顶点a点到顶点b的路程。那么这个中转的顶点k是1~n中的哪个点呢?甚至有时候不只通过一个点,而是经过两个点或者更多点中转会更短,即a->k1->k2b->或者a->k1->k2…->k->i…->b。比如上图中从4号城市到3号城市(4->3)的路程e[4][3]原本是12。如果只通过1号城市中转(4->1->3),路程将缩短为11(e[4][1]+e[1][3]=5+6=11)。其实1号城市到3号城市也可以通过2号城市中转,使得1号到3号城市的路程缩短为5(e[1][2]+e[2][3]=2+3=5)。所以如果同时经过1号和2号两个城市中转的话,从4号城市到3号城市的路程会进一步缩短为10。通过这个的例子,我们发现每个顶点都有可能使得另外两个顶点之间的路程变短。好,下面我们将这个问题一般化。

当任意两点之间不允许经过第三个点时,这些城市之间最短路程就是初始路程,如下。

这里写图片描述

如现在只允许经过1号顶点,求任意两点之间的最短路程,应该如何求呢?只需判断e[i][1]+e[1][j]是否比e[i][j]要小即可。e[i][j]表示的是从i号顶点到j号顶点之间的路程。e[i][1]+e[1][j]表示的是从i号顶点先到1号顶点,再从1号顶点到j号顶点的路程之和。其中i是1~n循环,j也是1~n循环,代码实现如下。

for(i=1;i<=n;i++)   
{   
    for(j=1;j<=n;j++)   
    {   
        if ( e[i][j] > e[i][1]+e[1][j] )   
            e[i][j] = e[i][1]+e[1][j];   
    }   
} 

在只允许经过1号顶点的情况下,任意两点之间的最短路程更新为:

这里写图片描述

通过上图我们发现:在只通过1号顶点中转的情况下,3号顶点到2号顶点(e[3][2])、4号顶点到2号顶点(e[4][2])以及4号顶点到3号顶点(e[4][3])的路程都变短了。

接下来继续求在只允许经过1和2号两个顶点的情况下任意两点之间的最短路程。如何做呢?我们需要在只允许经过1号顶点时任意两点的最短路程的结果下,再判断如果经过2号顶点是否可以使得i号顶点到j号顶点之间的路程变得更短。即判断e[i][2]+e[2][j]是否比e[i][j]要小,代码实现为如下。

//经过1号顶点   

 for(i=1;i<=n;i++)   
     for(j=1;j<=n;j++)   
          if (e[i][j] > e[i][1]+e[1][j])  e[i][j]=e[i][1]+e[1][j];   

 //经过2号顶点   

 for(i=1;i<=n;i++)   
     for(j=1;j<=n;j++)   
        if (e[i][j] > e[i][2]+e[2][j])  e[i][j]=e[i][2]+e[2][j]; 

在只允许经过1和2号顶点的情况下,任意两点之间的最短路程更新为:

这里写图片描述

通过上图得知,在相比只允许通过1号顶点进行中转的情况下,这里允许通过1和2号顶点进行中转,使得e[1][3]和e[4][3]的路程变得更短了。

同理,继续在只允许经过1、2和3号顶点进行中转的情况下,求任意两点之间的最短路程。任意两点之间的最短路程更新为:
这里写图片描述

最后允许通过所有顶点作为中转,任意两点之间最终的最短路程为:
这里写图片描述

整个算法过程虽然说起来很麻烦,但是代码实现却非常简单,核心代码只有五行:

 for(k=1;k<=n;k++)   
     for(i=1;i<=n;i++)   
        for(j=1;j<=n;j++)   
           if(e[i][j]>e[i][k]+e[k][j])   
              e[i][j]=e[i][k]+e[k][j]; 

这段代码的基本思想就是:最开始只允许经过1号顶点进行中转,接下来只允许经过1和2号顶点进行中转……允许经过1~n号所有顶点进行中转,求任意两点之间的最短路程。用一句话概括就是:从i号顶点到j号顶点只经过前k号点的最短路程。其实这是一种“动态规划”的思想,关于这个思想我们将在下面给出这个算法的完整代码:

1.#include <stdio.h>   
2.int main()   
3.{   
4.    int e[10][10],k,i,j,n,m,t1,t2,t3;   
5.    int inf=99999999; //用inf(infinity的缩写)存储一个我们认为的正无穷值   
6.    //读入n和m,n表示顶点个数,m表示边的条数   
7.    scanf("%d %d",&n,&m);   
8.  
9.    //初始化   
10.    for(i=1;i<=n;i++)   
11.         for(j=1;j<=n;j++)   
12.             if(i==j)    e[i][j]=0;   
else    e[i][j]=inf;   
13.    //读入边   
14.    for(i=1;i<=m;i++)   
15.    {   
16.         scanf("%d %d %d",&t1,&t2,&t3);   
17.         e[t1][t2]=t3;   
18.    }   
19.  
20.    //Floyd-Warshall算法核心语句   
21.    for(k=1;k<=n;k++)   
22.         for(i=1;i<=n;i++)   
23.             for(j=1;j<=n;j++)   
24.                 if(e[i][j]>e[i][k]+e[k][j] )   
25.                     e[i][j]=e[i][k]+e[k][j];   
26.  
27.    //输出最终的结果   
28.    for(i=1;i<=n;i++)   
29.    {   
30.         for(j=1;j<=n;j++)   
31.         {   
32.             printf("%10d",e[i][j]);   
33.         }   
34.         printf("\n");   
35.    }   
36.  
37.    return 0;   
38.} 

有一点需要注意的是:如何表示正无穷。我们通常将正无穷定义为99999999,因为这样即使两个正无穷相加,其和仍然不超过int类型的范围(C语言int类型可以存储的最大正整数是2147483647)。在实际应用中最好估计一下最短路径的上限,只需要设置比它大一点既可以。例如有100条边,每条边不超过100的话,只需将正无穷设置为10001即可。如果你认为正无穷和其它值相加得到一个大于正无穷的数是不被允许的话,我们只需在比较的时候加两个判断条件就可以了,请注意下面代码中带有下划线的语句。

1.//Floyd-Warshall算法核心语句   
2.for(k=1;k<=n;k++)   
3.  for(i=1;i<=n;i++)   
4.      for(j=1;j<=n;j++)   
5.      if(e[i][k]<inf && e[k][j]<inf && e[i][j]>e[i][k]+e[k][j])   
6.          e[i][j]=e[i][k]+e[k][j]; 

上面代码的输入数据样式为:

4 8   
1 2 2   
1 3 6   
1 4 4   
2 3 3   
3 1 7   
3 4 1   
4 1 5   
4 3 12 

第一行两个数为n和m,n表示顶点个数,m表示边的条数。
接下来m行,每一行有三个数t1、t2 和t3,表示顶点t1到顶点t2的路程是t3。
得到最终结果如下:

这里写图片描述

通过这种方法我们可以求出任意两个点之间最短路径。它的时间复杂度是O(N3)。令人很震撼的是它竟然只有五行代码,实现起来非常容易。正是因为它实现起来非常容易,如果时间复杂度要求不高,使用Floyd-Warshall来求指定两点之间的最短路或者指定一个点到其余各个顶点的最短路径也是可行的。当然也有更快的算法,请看下一节:Dijkstra算法。

另外需要注意的是:Floyd-Warshall算法不能解决带有“负权回路”(或者叫“负权环”)的图,因为带有“负权回路”的图没有最短路。例如下面这个图就不存在1号顶点到3号顶点的最短路径。因为1->2->3->1->2->3->…->1->2->3这样路径中,每绕一次1->-2>3这样的环,最短路就会减少1,永远找不到最短路。其实如果一个图中带有“负权回路”那么这个图则没有最短路。
这里写图片描述

此算法由Robert W. Floyd(罗伯特·弗洛伊德)于1962年发表在“Communications of the ACM”上。同年Stephen Warshall(史蒂芬·沃舍尔)也独立发表了这个算法。Robert W.Floyd这个牛人是朵奇葩,他原本在芝加哥大学读的文学,但是因为当时美国经济不太景气,找工作比较困难,无奈之下到西屋电气公司当了一名计算机操作员,在IBM650机房值夜班,并由此开始了他的计算机生涯。此外他还和J.W.J. Williams(威廉姆斯)于1964年共同发明了著名的堆排序算法HEAPSORT。Robert W.Floyd在1987年获得了图灵奖。

3、贝尔曼-福特算法(BellmanFord)

Dijkstra算法是处理单源最短路径的有效算法,但它局限于边的权值非负的情况,若图中出现权值为负的边,Dijkstra算法就会失效,求出的最短路径就可能是错的。

这时候,就需要使用其他的算法来求解最短路径,Bellman-Ford算法就是其中最常用的一个。该算法由美国数学家理查德•贝尔曼(Richard Bellman, 动态规划的提出者)和小莱斯特•福特(Lester Ford)发明。

适用条件&范围:

单源最短路径(从源点s到其它所有顶点v);

有向图&无向图(无向图可以看作(u,v),(v,u)同属于边集E的有向图);

边权可正可负(如有负权回路输出错误提示);

差分约束系统;

Bellman-Ford算法的流程如下:
给定图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).

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

测试代码如下:(下面为有向图的Bellman-Ford算法。。。。。)C/C++

//#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
//using namespace std;
//表示一条边
struct Edge
{
    int src, dest, weight;
};

//带权值的有向图
struct Graph
{
    // V 顶点的数量, E 边的数量
    int V, E;

    // 用边的集合 表示一个图
    struct Edge* edge;
};

// 创建图
struct Graph* createGraph(int V, int E)
{
    struct Graph* graph = (struct Graph*) malloc( sizeof(struct Graph) );
    graph->V = V;
    graph->E = E;

    graph->edge = (struct Edge*) malloc( graph->E * sizeof( struct Edge ) );

    return graph;
}

// 打印结果
void printArr(int dist[], int n)
{
    printf("Vertex   Distance from Source\n");
    for (int i = 0; i < n; ++i)
        printf("%d \t\t %d\n", i, dist[i]);
}

// 获得单源最短路径,同时检测 负权回路
void BellmanFord(struct Graph* graph, int src)
{
    int V = graph->V;
    int E = graph->E;
    int dist[V];

    // 第一步初始化
    for (int i = 0; i < V; i++)
        dist[i]   = INT_MAX;
    dist[src] = 0;

    // 第二步:松弛操作
    for (int i = 1; i <= V-1; i++)
    {
        for (int j = 0; j < E; j++)
        {
            int u = graph->edge[j].src;
            int v = graph->edge[j].dest;
            int weight = graph->edge[j].weight;
            if (dist[u] + weight < dist[v])
                dist[v] = dist[u] + weight;
        }
    }

    // 第三步: 检测负权回路.  上面的操作保证没有负权回路的存在,
    // 如果找到了更短的路径,则说明存在负权回路
    for (int i = 0; i < E; i++)
    {
        int u = graph->edge[i].src;
        int v = graph->edge[i].dest;
        int weight = graph->edge[i].weight;
        if (dist[u] + weight < dist[v])
            printf("Graph contains negative weight cycle");
    }

    printArr(dist, V);
    return;
}

// 测试
int main()
{
    /* 创建 例子中的那个图的结构 */
    int V = 5;
    int E = 8;
    struct Graph* graph = createGraph(V, E);

    // add edge 0-1 (or A-B in above figure)
    graph->edge[0].src = 0;
    graph->edge[0].dest = 1;
    graph->edge[0].weight = -1;

    // add edge 0-2 (or A-C in above figure)
    graph->edge[1].src = 0;
    graph->edge[1].dest = 2;
    graph->edge[1].weight = 4;

    // add edge 1-2 (or B-C in above figure)
    graph->edge[2].src = 1;
    graph->edge[2].dest = 2;
    graph->edge[2].weight = 3;

    // add edge 1-3 (or B-D in above figure)
    graph->edge[3].src = 1;
    graph->edge[3].dest = 3;
    graph->edge[3].weight = 2;

    // add edge 1-4 (or A-E in above figure)
    graph->edge[4].src = 1;
    graph->edge[4].dest = 4;
    graph->edge[4].weight = 2;

    // add edge 3-2 (or D-C in above figure)
    graph->edge[5].src = 3;
    graph->edge[5].dest = 2;
    graph->edge[5].weight = 5;

    // add edge 3-1 (or D-B in above figure)
    graph->edge[6].src = 3;
    graph->edge[6].dest = 1;
    graph->edge[6].weight = 1;

    // add edge 4-3 (or E-D in above figure)
    graph->edge[7].src = 4;
    graph->edge[7].dest = 3;
    graph->edge[7].weight = -3;

    BellmanFord(graph, 0);

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值