zoukankan      html  css  js  c++  java
  • 最短路算法 :Bellman-ford算法 & Dijkstra算法 & floyd算法 & SPFA算法 详解

                 本人QQ :2319411771   邮箱 : cyb19950118@163.com

         若您发现本文有什么错误,请联系我,我会及时改正的,谢谢您的合作!

         本文为原创文章,转载请注明出处 

         本文链接   :http://www.cnblogs.com/Yan-C/p/3916281.html 。

    很早就想写一下最短路的总结了,但是一直懒,就没有写,这几天又在看最短路,虽没什么长进,但还是加深了点理解。

    于是就想写一个大点的总结,要写一个全的。

    在本文中因为邻接表在比赛中不如前向星好写,而且前向星效率并不低所以,本文的代码 存图只有两种:前向星 or 邻接矩阵

    本文包含如下内容:

             1、Bellman-Ford算法

       2、Dijkstra算法(代码 以邻接矩阵为例)    &&    Dijkstra + 优先队列的优化(也就是堆优化)

       3、floyd-Warshall算法(代码 以邻接矩阵为例)

       4、SPFA(代码 以前向星为例)

       5、BFS 求解最短路

       6、路径还原

    序章    :

                在开始最短路的算法之前,需要先说明一下   松弛   (relax)

        松弛是什么? 这个问题在我刚刚开始接触最短路的时候也是一脸茫然啊。但是在读了《算法导论》后我知道了。有资源的同学可以看一下。

        不想看厚厚的书的同学看这儿 : 松弛其实很简单,就是      用现在的最小路径去更新其他的路径。用C/C++写其实就是这个样子。   

    1 if(dis[i]>dis[k]+G[k][i]){
    2     dis[i] = dis[k]+G[k][i];
    3 }
    4 //其中dis[i]  是其他的路径
    5 //dis[k]  是现在的最小路径
    6 //G[k][i]  是现在的最小路径的点到其他路径点的权值。

        在《算法导论》中松弛这一步  若条件成立 不仅更新了路径距离 && 更新了前驱。此处未写出。

    插播一下

         在讲完松弛操作之后,最短路算法开始之前,说一下什么是  最短路径的估计值

        我们的源点用s表示。

        在这里我们用数组   dis[N]  来存储最短路径,dis[N]数组为源点到其他点的最小距离。

        那么最最开始的最短路径的估计值 也就是对 dis[N] 的初始化喽。

        一般我们的初始化都是初始化为 dis[N] = +∞ , But 在一些时候是初始化为dis[N] = 0的(“一些时候”后面再讲)。

        But 源点是要初始化为0的, dis[s] = 0,因为s—>s的距离为0;

    1 #define MAX 9999999
    2  
    3 int dis[203];
    4  
    5 fill(dis,dis+n,MAX);//不知此函数的可以百度
    6 dis[s] = 0;

        我们也可以这样原始的初始化。

    1 #define MAX 9999999
    2  
    3 int dis[203];
    4 int i;
    5  
    6 for(i=0;i<n;i++)
    7     dis[i] = MAX;
    8 dis[s] = 0;

    1、Bellman-Ford算法 :

         bellman-ford 算法解决的是一般情况下的单源最短路径问题,其边可以为负值。bellman-ford算法可以判断图是否存在负环,若存在负环会返回一个布尔值。当然在没有负环存在的    情况下会返回所求的最短路径的值。

        bellman-ford() :算法如下

        1   图的初始化等操作

        2  for i = 1 to |G.V| - 1   //  |G.V|  为图 G的点的总数

        3    for each edge(u,v)∈G.E   //G.E 为图 G 的边

        4           relax(u,v,w) 也就是if  v.d>u.d+w(u,v)  , v.d = u.d+w(u,v);

        5  for each edge(u,v)∈G.E

        6     if v.d>u.d+w(u,v)  //v.d为出发源点到结点v的最短路径的估计值  u.d亦如此  w(u,v) 为u结点到v结点的权重值(通俗点就是u—>v的花费)。

        7      return false;

        8  return true

        此算法分为3步:

        1)  第1行对图进行初始化,初始化dis[N] = +∞,dis[s] = 0;

         2)  第2~4行为求最短路的过程,是对图的每一条边进行|V|-1次松弛操作,求取最短路径。

         3) 第5~8行为对每条边进行|V|-1次松弛后,检查是否存在负环并返回相应的布尔值,因为进行|V|-1次松弛后若没有负环则v.d的值确定不变,若有负环则会继续进行松弛操作,因为一个数+负数是一定比它本身要小的。

        此算法的 时间复杂度为O(VE)。

        

    eg :

        我们做一个简单的题练习一下:

        多组输入。第一行给你两个数n(代表点),m(代表边)

        第2—m+1行 ,每行三个数u,v,  w。0<=u,v<n,  w>=0;

        第m+2行两个数 s, t  。 s为源点,t为要到达的目的点。

        求s到t 的最短路,若存在最短路输出最短路的值,否则输出-1。

    这也就是hdu 的题目  传送门     

     1 #include <cstdio>
     2 #include <cstring>
     3 #include <algorithm>
     4 #include <iostream>
     5 #include <queue>
     6 #define MAX 9999999
     7 
     8 using namespace std;
     9 
    10 struct node
    11 {
    12     int u, v, w;
    13 };
    14 node edge[2003];
    15 int n, m, s, t;
    16 
    17 void bellman_ford()
    18 {
    19     int i, j;
    20     bool flag;//用于优化的
    21     int dis[203];//保存最短路径
    22     //初始化
    23     fill(dis,dis+n,MAX);//其他点为+∞
    24     dis[s] = 0;//源点初始化为0
    25      m = m<<1;//此处和m = 2*m是一样的,因为建立的无向图
    26     for(i=1;i<n;i++)//进行|V|-1次
    27     {
    28         flag = false;//刚刚开始标记为假
    29         for(j=0;j<m;j++)//对每个边
    30         {   
    31             //if  (v.d>u.d+w(u,v))
    32             if(dis[edge[j].u]>dis[edge[j].v]+edge[j].w){//进行松弛操作
    33                 dis[edge[j].u] = dis[edge[j].v]+edge[j].w;//松弛成功
    34                 flag = true;//若松弛成功则标记为真
    35             }
    36         }
    37         if(!flag)//若所有的边i的循环中没有松弛成功的
    38             break;//退出循环
    39         //此优化可以大大提高效率。
    40     }
    41     printf("%d
    ",dis[t]==MAX?-1:dis[t]);//输出结果
    42 }
    43 
    44 int main()
    45 {
    46     int i;
    47 
    48     while(scanf("%d %d",&n,&m)==2){//输入点的总数n,边的总数m
    49         for(i=0;i<m;i++)
    50         {
    51             scanf("%d %d %d",&edge[i].u,&edge[i].v,&edge[i].w);//每条边的u,v,w的输入
    52             edge[i+m].u = edge[i].v;//因为为无向图所以u—>v和v—>u 是一样的
    53             edge[i+m].v = edge[i].u;//So...
    54             edge[i+m].w = edge[i].w;//So...
    55         }
    56         scanf("%d %d",&s,&t);//起点和终点
    57         bellman_ford();//调用算法部分
    58     }
    59     return 0;
    60 }
    题解在此

    说明  : 因为此图w>=0,所以是一定没有负环的,因此没有 3)第5~8行的操作

    对于bellman-ford算法 推荐使用结构体数组存储,因为比较方便和简洁,当然也可以用其他的数据结构。

    用到的数据结构

    1 struct node
    2 {
    3     int u, v, w;//u 为起点,v为终点,w为u—>v的权值
    4 };
    5 node edge[2003];

    主函数对边的读取和存储

     1 int main()
     2 {
     3     int i;
     4 
     5     while(scanf("%d %d",&n,&m)==2){//输入点的总数n,边的总数m
     6         for(i=0;i<m;i++)
     7         {
     8             scanf("%d %d %d",&edge[i].u,&edge[i].v,&edge[i].w);//每条边的u,v,w的输入
     9             edge[i+m].u = edge[i].v;//因为为无向图所以u—>v和v—>u 是一样的
    10             edge[i+m].v = edge[i].u;//So...
    11             edge[i+m].w = edge[i].w;//So...
    12         }
    13         scanf("%d %d",&s,&t);//起点和终点
    14         bellman_ford();//调用算法部分
    15     }
    16     return 0;
    17 }

    bellman-ford算法求最短路 C/C++版

     1 void bellman_ford()
     2 {
     3     int i, j;
     4     bool flag;//用于优化的
     5     int dis[203];//保存最短路径
     6     //初始化
     7     fill(dis,dis+n,MAX);//其他点为+∞
     8     dis[s] = 0;//源点初始化为0
     9      m = m<<1;//此处和m = 2*m是一样的,因为建立的无向图
    10     for(i=1;i<n;i++)//进行|V|-1次
    11     {
    12         flag = false;//刚刚开始标记为假
    13         for(j=0;j<m;j++)//对每个边
    14         {   
    15             //if  (v.d>u.d+w(u,v))
    16             if(dis[edge[j].u]>dis[edge[j].v]+edge[j].w){//进行松弛操作
    17                 dis[edge[j].u] = dis[edge[j].v]+edge[j].w;//松弛成功
    18                 flag = true;//若松弛成功则标记为真
    19             }
    20         }
    21         if(!flag)//若所有的边i的循环中没有松弛成功的
    22             break;//退出循环
    23         //此优化可以大大提高效率。
    24     }
    25     printf("%d
    ",dis[t]==MAX?-1:dis[t]);//输出结果
    26 }

    对于优化 的解释:若图中存在负环的情况下外循环需要|V|-1次循环,若不存在负环,平均情况下的循环次数是要小于|V|-1次,当所有边没有松弛操作的时候我们就得到了

    最后的答案,没有必要继续循环下去,So有了这个简单的优化。

        对于bellman-ford算法求最短路  没有负环的情况下已经说明了,下面说一下求负环的强大功能

        eg. 题目传送门  点我   题解  

     1 #include <cstdio>
     2 #include <cstring>
     3 #include <algorithm>
     4 #include <iostream>
     5 #include <queue>
     6 #define MAX 9999999
     7 
     8 using namespace std;
     9 
    10 struct node
    11 {
    12     int u, v, w;//u 为起点,v为终点,w为u—>v的权值
    13 };
    14 node edge[5203];
    15 int n, m;//n 点数   m 边数
    16 
    17 bool bellman_ford()
    18 {
    19     int i, j;
    20     bool flag;
    21     int dis[503];//保存最短路径
    22 
    23     fill(dis,dis+n,MAX);//初始化
    24     dis[1] = 0;//因为判断是否有负环,对整个图而言,So  s = 1;
    25     //一下部分为 2) 第2~4行的操作
    26     for(i=1;i<n;i++)//共需进行|V|-1次
    27     {
    28         flag = false;//优化   初始化为假
    29         for(j=0;j<m;j++)//对每一条边
    30         {
    31             // if  u.d>v.d+w(u,v) , u.d = v.d+w(u,v);
    32             if(dis[edge[j].u]>dis[edge[j].v]+edge[j].w){//进行松弛
    33                 dis[edge[j].u] = dis[edge[j].v]+edge[j].w;//松弛操作成功
    34                 flag = true;//松弛成功变为真
    35             }
    36         }
    37         if(!flag)//若每条边没有松弛
    38             break;//跳出循环
    39     }
    40     // 一下部分为 3) 第5~8行的操作
    41     for(i=0;i<m;i++)
    42         if(dis[edge[i].u]>dis[edge[i].v]+edge[i].w)//进行|V|-1次操作后  有边还能进行松弛  说明
    43             return true;//存在负环
    44     return false;//不存在负环
    45 }
    46 
    47 int main()
    48 {
    49     int t, k, i;
    50 
    51     scanf("%d",&t);//输入测试数据的组数
    52     while(t-- && scanf("%d %d %d",&n,&m,&k)){//输入点数,正边数,负边数
    53         for(i=0;i<m;i++)
    54         {
    55             scanf("%d %d %d",&edge[i].u,&edge[i].v,&edge[i].w);//输入u,v,w;
    56             edge[i+m].u = edge[i].v;//双向
    57             edge[i+m].v = edge[i].u;//双向
    58             edge[i+m].w = edge[i].w;//双向
    59         }
    60         m <<= 1;//正边为双向 所以m = m*2;
    61         for(i=m;i<m+k;i++)
    62         {
    63             scanf("%d %d %d",&edge[i].u,&edge[i].v,&edge[i].w);//存负边数(单向)
    64             edge[i].w = -edge[i].w;//负边就要是负的
    65         }
    66         m += k;//单向,So不需要*2
    67         printf("%s
    ",bellman_ford()?"YES":"NO");//输出结果
    68     }
    69     return 0;
    70 }
    报告!!!

        题目大意: 第一行 输入一个数  是表示几组测试数据

        第二行  三个数 N(点的个数),M(正边的个数),W(负边的个数) 注意 :正边为双向的,负边为单向的。

        然后 M行u,v,w;

        再然后W行u,v,w;

        求这个图是不是存在负环。 有 YES 没NO。

        所用数据结构 :

    1 struct node
    2 {
    3     int u, v, w;//u 为起点,v为终点,w为u—>v的权值
    4 };
    5 node edge[5203];

        主函数对数据的获取。

     1 int main()
     2 {
     3     int t, k, i;
     4 
     5     scanf("%d",&t);//输入测试数据的组数
     6     while(t-- && scanf("%d %d %d",&n,&m,&k)){//输入点数,正边数,负边数
     7         for(i=0;i<m;i++)
     8         {
     9             scanf("%d %d %d",&edge[i].u,&edge[i].v,&edge[i].w);//输入u,v,w;
    10             edge[i+m].u = edge[i].v;//双向
    11             edge[i+m].v = edge[i].u;//双向
    12             edge[i+m].w = edge[i].w;//双向
    13         }
    14         m <<= 1;//正边为双向 所以m = m*2;
    15         for(i=m;i<m+k;i++)
    16         {
    17             scanf("%d %d %d",&edge[i].u,&edge[i].v,&edge[i].w);//存负边数(单向)
    18             edge[i].w = -edge[i].w;//负边就要是负的
    19         }
    20         m += k;//单向,So不需要*2
    21         printf("%s
    ",bellman_ford()?"YES":"NO");//输出结果
    22     }
    23     return 0;
    24 }

        bellman-ford  算法求解

     1 bool bellman_ford()
     2 {
     3     int i, j;
     4     bool flag;
     5     int dis[503];//保存最短路径
     6 
     7     fill(dis,dis+n,MAX);//初始化
     8     dis[1] = 0;//因为判断是否有负环,对整个图而言,So  s = 1;
     9     //一下部分为 2) 第2~4行的操作
    10     for(i=1;i<n;i++)//共需进行|V|-1次
    11     {
    12         flag = false;//优化   初始化为假
    13         for(j=0;j<m;j++)//对每一条边
    14         {
    15             // if  u.d>v.d+w(u,v) , u.d = v.d+w(u,v);
    16             if(dis[edge[j].u]>dis[edge[j].v]+edge[j].w){//进行松弛
    17                 dis[edge[j].u] = dis[edge[j].v]+edge[j].w;//松弛操作成功
    18                 flag = true;//松弛成功变为真
    19             }
    20         }
    21         if(!flag)//若每条边没有松弛
    22             break;//跳出循环
    23     }
    24     // 一下部分为 3) 第5~8行的操作
    25     for(i=0;i<m;i++)
    26         if(dis[edge[i].u]>dis[edge[i].v]+edge[i].w)//进行|V|-1次操作后  有边还能进行松弛  说明
    27             return true;//存在负环
    28     return false;//不存在负环
    29 }

        上面只是第一种对负环存在的判断,继续下一种:

        我们前面已经说过  若没有负环外循环最多进行|V|-1次即可,就可得到最短路径,那么若存在负环,则第|V|次操作就说明存在负环。

     1 bool bellman_ford()
     2 {
     3     int i, j;
     4     bool flag;
     5     int dis[503];//保存最短路径
     6 
     7     fill(dis,dis+n,MAX);//初始化
     8     dis[1] = 0;//因为判断是否有负环,对整个图而言,So  s = 1;
     9     //一下部分为 2) 第2~4行的操作
    10     for(i=0;i<n;i++)//共需进行|V|-1次
    11     {
    12         flag = false;//优化   初始化为假
    13         for(j=0;j<m;j++)//对每一条边
    14         {
    15             // if  u.d>v.d+w(u,v) , u.d = v.d+w(u,v);
    16             if(dis[edge[j].u]>dis[edge[j].v]+edge[j].w){//进行松弛
    17                 dis[edge[j].u] = dis[edge[j].v]+edge[j].w;//松弛操作成功
    18                 flag = true;//松弛成功变为真
    19             }
    20         }
    21         if(!flag)//若每条边没有松弛
    22             break;//跳出循环
    23         //下面这一部分代替了  3) 第5~8行的操作
    24         //因为对于V个点 你最多需要进行|V|-1次外循环,如果有负环它会一直进行下去,但是只要进行到第V次的时候就说明存在负环了
    25         if(i == n-1)//若有
    26             return true;//返回有负环
    27     }
    28     return false;//不存在负环
    29 }

        bellman-ford 算法也说的差不多了,对于此算法的SPFA优化 ,我们在本文的后面部分单独讲解。

        不知大家还记不记的上面的那个许多个But 中的那个But dis[N] = 0呢?

        给大家留下一个问题吧。

        这个问题是《算法导论》上的一个思考题,问题是这样的 :

        你如果知道一个带权重的有向图中 存在一个负环,那么请你设计一个有效&&正确的算法列出所有属于该环路上的结点。

        如果你有什么想法  请跟我一起分享下吧 

        本人QQ :2319411771   邮箱 : cyb19950118@163.com

      

    2、dijkstra算法 :   (贪心策略)

        Dijkstra算法解决的是带权重的有向图上单源最短路径问题,该算法要求所有边的权重都为正值。

        在此有的同学可能就要问了,为什么不能处理负值呢?

        Why????

        Dijkstra算法不是绝对的不能处理权重为负值,而是因为这个负值的大小和所在位置需要特别要求才可应用&&求得正确结果。

        但我们的平时所遇到的是一般情况下的,是需要算法有通用性的,所以就要求所有边的权重都为正值。

        在本文我此算法的后面部分我会给出两个例子,分别为   不可以有负边 和 可以有负边的例子。为什么在此不先给出呢?  

        Why?????

        如果还不知道Dijkstra算法又怎么会 看懂这两个例子呢?  So  看完这个算法 再看例子吧。

        Dijkstra算法在运行过程中维持的关键信息是一组结点集合S。从源结点s 到该集合中每个结点之间的最短路径都已经被找到。算法重复从结点集V-S中选择最短路径估计最小的结点u,讲u加入到    集合S,然后对所有从u发出的边进行松弛。

        Dijkstra 算法如下://这个描述使用的最小优先队列Q来保存结点集合,每个结点的关键值为其d值。

        1   对图的建立和处理,dis[N]数组的初始化等等操作

        2    S = 

        3    Q = G.V

        4  while Q ≠ ∅

        5    u = EXTRACT-MIN(Q)

        6    S = S ∪ {u}

        7     for each vertex v∈ G.Adj[u]

        8                relax(u,v,w)

        此算法在此分为二步 : 第二大步中又分为3小步

        1)   第1~3行 对dis[N]数组等的初始化,集合S 为∅,Q集合为G.V操作

        2)   第4~8行 ① 第4行 进行G.V次操作

               ② 第5~行 从Q中找到一个点,这个点是Q中所有的点   s—>某点  最小的最短路径的点,并将此点加入S集合

               ③ 第7~8行  进行松弛操作,用此点来更新其他路径的距离。

        对于邻接矩阵存储的图 来说此算法的时间复杂度为 O(|V|²),用其他的数据结构可以优化为O(|E|log|V|)的时间复杂度。

        对于本文所说的其他数据结构 使用的为前向星,对于前向星是不能出现负边的。

        我们先看邻接矩阵存储的图的情况。

        还是hdu的那道题  题目传送门     题解 :

     1 #include <cstdio>
     2 #include <cstring>
     3 #include <algorithm>
     4 #include <iostream>
     5 #include <queue>
     6 #define MAX 9999999
     7 
     8 using namespace std;
     9 
    10 int G[203][203];//二维数组 图的存储
    11 int n, s, t;//n 点的个数 , s 起点 ,t 终点
    12 
    13 void dijkstra()
    14 {
    15     bool vis[203];//相当于集合Q的功能, 标记该点是否访问过
    16     int dis[203];//保存最短路径
    17     int i, j, k;
    18 
    19     for(i=0;i<n;i++)//初始化
    20         dis[i] = G[s][i];//s—>各个点的距离
    21     memset(vis,false,sizeof(vis));//初始化为假 表示未访问过
    22     dis[s] = 0;//s->s 距离为0
    23     vis[s] = true;//s点访问过了,标记为真
    24     for(i=1;i<n;i++)//G.V-1次操作+上面对s的访问 = G.V次操作
    25     {
    26         k = -1;
    27         for(j=0;j<n;j++)//从尚未访问过的点中选一个距离最小的点
    28             if(!vis[j] && (k==-1||dis[k]>dis[j]))//未访问过 && 是距离最小的
    29                 k = j;
    30         if(k == -1)//若图是不连通的则提前结束
    31             break;//跳出循环
    32         vis[k] = true;//将k点标记为访问过了
    33         for(j=0;j<n;j++)//松弛操作
    34             if(!vis[j] && dis[j]>dis[k]+G[k][j])//该点为访问过 && 可以进行松弛
    35                 dis[j] = dis[k]+G[k][j];//j点的距离  大于当前点的距离+w(k,j) 则松弛成功,进行更新
    36     }
    37     printf("%d
    ",dis[t]==MAX?-1:dis[t]);//输出结果
    38 }
    39 
    40 int main()
    41 {
    42     int m, i, j, u, v, w;
    43 
    44     while(scanf("%d %d",&n,&m)==2){//获取点的个数 边的个数
    45         for(i=0;i<n;i++)
    46             for(j=0;j<n;j++)
    47                 G[i][j] = i==j?0:MAX;//初始化,本身到本身的距离为0,其他的为无穷大
    48         while(m--){
    49             scanf("%d %d %d",&u,&v,&w);//获取u,v,w(u,v);
    50             if(G[u][v]>w)//因为初始化的操作  && 若有重边要去最小的权重值
    51                 G[u][v] = G[v][u] = w;//无向图 双向
    52         }
    53         scanf("%d %d",&s,&t);//获取起止点
    54         dijkstra();
    55     }
    56     return 0;
    57 }
    点我查看

        应用的数据结构

    1 int G[203][203];//二维数组 图的存储

        主函数对数据的获取

     1 int main()
     2 {
     3     int m, i, j, u, v, w;
     4 
     5     while(scanf("%d %d",&n,&m)==2){//获取点的个数 边的个数
     6         for(i=0;i<n;i++)
     7             for(j=0;j<n;j++)
     8                 G[i][j] = i==j?0:MAX;//初始化,本身到本身的距离为0,其他的为无穷大
     9         while(m--){
    10             scanf("%d %d %d",&u,&v,&w);//获取u,v,w(u,v);
    11             if(G[u][v]>w)//因为初始化的操作  && 若有重边要去最小的权重值
    12                 G[u][v] = G[v][u] = w;//无向图 双向
    13         }
    14         scanf("%d %d",&s,&t);//获取起止点
    15         dijkstra();
    16     }
    17     return 0;
    18 }

        Dijkstra算法

     1 void dijkstra()
     2 {
     3     bool vis[203];//相当于集合Q的功能, 标记该点是否访问过
     4     int dis[203];//保存最短路径
     5     int i, j, k;
     6 
     7     for(i=0;i<n;i++)//初始化
     8         dis[i] = G[s][i];//s—>各个点的距离
     9     memset(vis,false,sizeof(vis));//初始化为假 表示未访问过
    10     dis[s] = 0;//s->s 距离为0
    11     vis[s] = true;//s点访问过了,标记为真
    12     for(i=1;i<n;i++)//G.V-1次操作+上面对s的访问 = G.V次操作
    13     {
    14         k = -1;
    15         for(j=0;j<n;j++)//从尚未访问过的点中选一个距离最小的点
    16             if(!vis[j] && (k==-1||dis[k]>dis[j]))//未访问过 && 是距离最小的
    17                 k = j;
    18         if(k == -1)//若图是不连通的则提前结束
    19             break;//跳出循环
    20         vis[k] = true;//将k点标记为访问过了
    21         for(j=0;j<n;j++)//松弛操作
    22             if(!vis[j] && dis[j]>dis[k]+G[k][j])//该点为访问过 && 可以进行松弛
    23                 dis[j] = dis[k]+G[k][j];//j点的距离  大于当前点的距离+w(k,j) 则松弛成功,进行更新
    24     }
    25     printf("%d
    ",dis[t]==MAX?-1:dis[t]);//输出结果
    26 }

        另一种 用其他的数据结构可以优化为O(|E|log|V|)的时间复杂度。

        使用STL中的最小优先队列 priority_queue,进行优化。

         题目继续使用此题。

     1 #include <cstdio>
     2 #include <cstring>
     3 #include <algorithm>
     4 #include <iostream>
     5 #include <queue>
     6 #define MAX 9999999
     7 
     8 using namespace std;
     9 //pair 的first 保存的为最短距离, second保存的为顶点编号
    10 typedef pair<int, int >P;//对组  不知道请自行百度   
    11 
    12 struct node
    13 {
    14     int v, w;//v 为到达的点, w为权重
    15     int next;//记录下一个结构体的位置 ,就向链表的next功能是一样的
    16 };
    17 node edge[2003];//存所有的边,因为是无向图,所以*2
    18 int cnt;//结构体的下标
    19 int n, s, t;//n 点数,s 起点,t止点
    20 int head[203];//和链表的头指针数组是一样的。只不过此处head[u]记录的为最后加入 edge 的且与u相连的边在 edge 中的位置,即下标
    21 
    22 void add(int u, int v, int w)//加边操作
    23 {
    24     edge[cnt].v = v;
    25     edge[cnt].w = w;
    26     edge[cnt].next = head[u];//获得下一个结构体的位置
    27     head[u] = cnt++;//记录头指针的下标
    28 }
    29 
    30 void dijkstra()
    31 {
    32     int dis[203];//最短路径数组
    33     int i, v;//v保存从队列中取出的数的第二个数  也就是顶点的编号
    34     priority_queue<P,vector<P>,greater<P> >que;//优先队列 从小到大
    35     node e;//保存边的信息,为了书写方便
    36     P p;//保存从队列取出的数值
    37 
    38     fill(dis,dis+n,MAX);//初始化,都为无穷大
    39     dis[s] = 0;//s—>s  距离为0
    40     que.push(P(0,s));//放入距离 为0   点为s
    41     while(!que.empty()){
    42         p = que.top();//取出队列中最短距离最小的对组
    43         que.pop();//删除
    44         v = p.second;//获得最短距离最小的顶点编号
    45         if(dis[v] < p.first)//若取出的不是最短距离
    46             continue;//则进行下一次循环
    47         for(i=head[v];i!=-1;i=edge[i].next)//对与此点相连的所有的点进行遍历
    48         {
    49             e = edge[i];//为了书写的方便。
    50             if(dis[e.v]>dis[v]+e.w){//进行松弛
    51                 dis[e.v]=dis[v]+e.w;//松弛成功
    52                 que.push(P(dis[e.v],e.v));//讲找到的松弛成功的距离 和顶点放入队列
    53             }
    54         }
    55     }
    56     printf("%d
    ",dis[t]==MAX?-1:dis[t]);//输出结果
    57 }
    58 
    59 int main()
    60 {
    61     int m, u, v, w;
    62 
    63     while(scanf("%d %d",&n,&m)==2){//获取点数  边数
    64         cnt = 0;//结构体下标从0开始
    65         memset(head,-1,sizeof(head));//初始化head[N]数组
    66         while(m--){
    67             scanf("%d %d %d",&u,&v,&w);//获取u,v,w(u,v)
    68             add(u,v,w);//加边
    69             add(v,u,w);//加边
    70         }
    71         scanf("%d %d",&s,&t);//获取起止点
    72         dijkstra();
    73     }
    74     return 0;
    75 }
    题解在此

        用到的数据结构 :前向星 对组 优先队列

        

     1 //pair 的first 保存的为最短距离, second保存的为顶点编号
     2 typedef pair<int, int >P;//对组  不知道请自行百度   
     3 
     4 struct node//前向星存边
     5 {
     6     int v, w;//v 为到达的点, w为权重
     7     int next;//记录下一个结构体的位置 ,就向链表的next功能是一样的
     8 };
     9 node edge[2003];//存所有的边,因为是无向图,所以*2
    10 int head[203];//和链表的头指针数组是一样的。只不过此处head[u]记录的为最后加入 edge 的且与u相连的边在 edge 中的位置,即下标
    11 
    12 priority_queue<P,vector<P>,greater<P> >que;//优先队列 从小到大

        在此我们说一下前向星的加边函数

    1 void add(int u, int v, int w)//加边操作
    2 {
    3     edge[cnt].v = v;
    4     edge[cnt].w = w;
    5     edge[cnt].next = head[u];//获得下一个结构体的位置
    6     head[u] = cnt++;//记录头指针的下标
    7 }

        主函数对数据的获取

     1 int main()
     2 {
     3     int m, u, v, w;
     4 
     5     while(scanf("%d %d",&n,&m)==2){//获取点数  边数
     6         cnt = 0;//结构体下标从0开始
     7         memset(head,-1,sizeof(head));//初始化head[N]数组
     8         while(m--){
     9             scanf("%d %d %d",&u,&v,&w);//获取u,v,w(u,v)
    10             add(u,v,w);//加边
    11             add(v,u,w);//加边
    12         }
    13         scanf("%d %d",&s,&t);//获取起止点
    14         dijkstra();
    15     }
    16     return 0;
    17 }

        Dijkstra算法求值

     1 void dijkstra()
     2 {
     3     int dis[203];//最短路径数组
     4     int i, v;//v保存从队列中取出的数的第二个数  也就是顶点的编号
     5     priority_queue<P,vector<P>,greater<P> >que;//优先队列 从小到大
     6     node e;//保存边的信息,为了书写方便
     7     P p;//保存从队列取出的数值
     8 
     9     fill(dis,dis+n,MAX);//初始化,都为无穷大
    10     dis[s] = 0;//s—>s  距离为0
    11     que.push(P(0,s));//放入距离 为0   点为s
    12     while(!que.empty()){
    13         p = que.top();//取出队列中最短距离最小的对组
    14         que.pop();//删除
    15         v = p.second;//获得最短距离最小的顶点编号
    16         if(dis[v] < p.first)//若取出的不是最短距离
    17             continue;//则进行下一次循环
    18         for(i=head[v];i!=-1;i=edge[i].next)//对与此点相连的所有的点进行遍历
    19         {
    20             e = edge[i];//为了书写的方便。
    21             if(dis[e.v]>dis[v]+e.w){//进行松弛
    22                 dis[e.v]=dis[v]+e.w;//松弛成功
    23                 que.push(P(dis[e.v],e.v));//讲找到的松弛成功的距离 和顶点放入队列
    24             }
    25         }
    26     }
    27     printf("%d
    ",dis[t]==MAX?-1:dis[t]);//输出结果
    28 }

        自此Dijkstra算法就算接近尾声了,现在还大家一个债,那就是前面的Why

     在此给出的是百度知道上的一位网友给的解释 :

        dijkstra由于是贪心的,每次都找一个距源点最近的点(dmin),然后将该距离定为这个点到源点的最短路径(d[i]<--dmin);但如果存在负权边,那就有可能先通过并不是距源点最近的一个次优点(dmin'),再通过这个负权边L(L<0),使得路径之和更小(dmin'+L<dmin),则dmin'+L成为最短路径,并不是dmin,这样dijkstra就被囧掉了

      比如n=3,邻接矩阵:

      0,3,4

      3,0,-2

      4,-2,  0

     用dijkstra求得d[1,2]=3,事实上d[1,2]=2,就是通过了1-3-2使得路径减小。
     这就是为什么Dijkstra不能处理负边的情况。

     再给出可以使用Dijkstra && 带负边的情况
     n = 3,邻接矩阵
     0, 3, 4
     3, 0, -1
     4, -1, 0
     dis[1,2] = 3, dis[1,3] = 2 是正确的。(为邻接矩阵的存图方式下的)
     
     此算法讲解结束。

      
    3、floyd-Warshall算法 :   (动态规划)
     
      floyd算法是一个很强大的算法,它可以计算任意两点之间的最短路径,其边可以为负值。
      对于floyd算法是我刚刚开始接触最短路算法中最喜欢的了,因为它的代码简短,便于理解,而且功能也很强大,虽然有点短腿但我还是很喜欢这个代码。
      floyd算法是三重for 的嵌套。对于这个算法给出《挑战程序设计》中的证明 :
    证明:
      对于0~k,我们分i到j的最短路正好经过顶点k一次和完全不经过顶点k两种情况来讨论。不仅过顶点k的情况下,d[k][i][j] = d[k-1][i][j]。通过顶点k的情况,d[k][i][j]
      = d[k-1][i][k]+d[k-1][k][j]。合起来就得到了d[k][i][j] = min(d[k-1][i][j],d[k-1][i][k]+d[k-1][k][j])。这个DP也可以用同一个数组不断进行如下的操作:
      d[i][j] = min(d[i][j],d[i][k]+d[k][j])的更新来实现。
      floyd算法的时间复杂度为O(|V|
    ³)。 450*450*450<10的8次方
      下面给出floyd算法的程序。
      
     1 void floyd()
     2 {
     3     int i, j, k;
     4 
     5     for(k=0;k<n;k++)
     6         for(i=0;i<n;i++)
     7             for(j=0;j<n;j++)
     8                 G[i][j] = min(G[i][j],G[i][k]+G[k][j]);
     9     printf("%d
    ",G[s][t]==MAX?-1:G[s][t]);
    10 }

      在此给出图的初始化和数据的读取。

     1 int main()
     2 {
     3     int i, j, m, u, v, w;
     4 
     5     while(scanf("%d %d",&n,&m)==2){
     6         for(i=0;i<n;i++)
     7             for(j=0;j<n;j++)
     8                 G[i][j] = i==j?0:MAX;
     9         while(m--){
    10             scanf("%d %d %d",&u,&v,&w);
    11             if(G[u][v]>w)
    12                 G[u][v] = G[v][u] = w;
    13         }
    14         scanf("%d %d",&s,&t);
    15         floyd();
    16     }
    17     return 0;
    18 }

        对floyd算法呢,因为他的简洁,在此就不多说。

        补充一下:对于floyd判断负环是否存在只需检查是否存在d[i][i]是负数的顶点i 即可。

        

    4、 SPFA算法   (bellman-ford算法的优化)

        SPFA算法是西南交通大学段凡丁于1994年发表的。

        SPFA算法 :设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短    路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。

        SPFA 是这样判断负环的: 如果某个点进入队列的次数超过N次则存在负环(SPFA无法处理带负环的图)

         期望的时间复杂度:O(ke), 其中k为所有顶点进队的平均次数,可以证明k一般小于等于2。

        SPFA算法有两个优化算法 SLF 和 LLL: SLF:Small Label First 策略,设要加入的节点是j,队首元素为i,若dist(j)<dist(i),则将j插入队首,否则插入队尾。 LLL:Large Label Last     策略,设队首元素为i,队列中所有dist值的平均值为x,若dist(i)>x则将i插入到队尾,查找下一元素,直到找到某一i使得dist(i)<=x,则将i出队进行松弛操作。 SLF 可使速度提高 15     ~ 20%;SLF + LLL 可提高约 50%。 在实际的应用中SPFA的算法时间效率不是很稳定,为了避免最坏情况的出现,通常使用效率更加稳定的Dijkstra算法。

        SPFA() :

        1   对图的建立和处理,dis[N]数组的初始化等等操作

        2   Q += s //Q 为一个队列  s为源点

        3   while Q ≠ ∅//队列不为空

        4    u = Q中的点//从Q中取出一个点u

        5   把u点标记为为访问过的

        6    for each vertex v∈ G.Adj[u]//对所有的边

        7        relax(u,v,w)//进行松弛

        8                  if(v  未被访问过)//若v未被访问过

        9            Q += v;//加入队列

        以上伪代码为自己写的,希望能看。

        此算法分为3部分 :

        1)  第1~2行  建图对dis[N]和vis[N]数组等数组进行初始化。 若判断负环需要加一个flag[N]数组,初始化为0,某点 u 若加入Q队列一次,怎flag[u]++,若flag[u]>=n,说明u进入队列的次数大于点的个数,因此此图存在负环,返回一个布尔值。

          2)  第3行当队列不为空的时候进行操作

         3)  第4~9行 取出Q中的点u ,用u对所有的边进行松弛操作,若松弛成功,判断该点v是否被访问过,若未访问过加入Q队列中。

        继续以poj的虫洞为例题

        题目传送门   

     1 #include <cstdio>
     2 #include <cstring>
     3 #include <algorithm>
     4 #include <iostream>
     5 #include <queue>
     6 #define MAX 9999999
     7 
     8 using namespace std;
     9 
    10 int G[503][503];
    11 int n;
    12 
    13 bool SPFA()
    14 {
    15     int u;
    16     int i;
    17     queue<int >que;
    18     int dis[503];
    19     bool vis[503];
    20     int flag[503];
    21 
    22     memset(flag,0,sizeof(flag));
    23     memset(vis,false,sizeof(vis));
    24     fill(dis,dis+n+1,MAX);
    25     dis[1] = 0;
    26     que.push(1);
    27     while(!que.empty()){
    28         u = que.front();
    29         que.pop();
    30         vis[u] = false;
    31         for(i=1;i<=n;i++)
    32         {
    33             if(dis[i]>dis[u]+G[u][i]){
    34                 dis[i] = dis[u]+G[u][i];
    35                 if(!vis[i]){
    36                     vis[i] = true;
    37                     flag[i]++;
    38                     if(flag[i]>=n)
    39                         return true;
    40                     que.push(i);
    41                 }
    42             }
    43         }
    44     }
    45     return false;
    46 }
    47 
    48 int main()
    49 {
    50     int t, k, i, j, u, v, w, m;
    51 
    52     scanf("%d",&t);
    53     while(t--){
    54         scanf("%d %d %d",&n,&m,&k);
    55         for(i=1;i<=n;i++)
    56             for(j=1;j<=n;j++)
    57                 G[i][j] = i==j?0:MAX;
    58         for(i=0;i<m;i++)
    59         {
    60             scanf("%d %d %d",&u,&v,&w);
    61             if(G[u][v]>w)
    62                 G[u][v] = G[v][u] = w;
    63         }
    64         for(i=0;i<k;i++)
    65         {
    66             scanf("%d %d %d",&u,&v,&w);
    67             G[u][v] = -w;
    68         }
    69         printf("%s
    ",SPFA()?"YES":"NO");
    70     }
    71     return 0;
    72 }
    题解 Here!Here!

        SPFA算法

     1 bool SPFA()
     2 {
     3     int u;//从队列Q中取出的数
     4     int i;
     5     queue<int >que;//Q队列
     6     int dis[503];//保存最短距离
     7     bool vis[503];//访问数组
     8     int flag[503];//记录点进入队列的次数
     9 
    10     memset(flag,0,sizeof(flag));//初始化为0
    11     memset(vis,false,sizeof(vis));//初始化
    12     fill(dis,dis+n+1,MAX);//初始化
    13     dis[1] = 0;//从  1 开始
    14     que.push(1);//将 1 放入队列
    15     while(!que.empty()){//Q 不为空
    16         u = que.front();//从Q中取出一个数
    17         que.pop();//删除此数
    18         vis[u] = false;//标记为未访问过
    19         for(i=1;i<=n;i++)//对所有的边
    20         {
    21             if(dis[i]>dis[u]+G[u][i]){//进行松弛
    22                 dis[i] = dis[u]+G[u][i];//松弛成功
    23                 if(!vis[i]){//若点i 未被访问过
    24                     vis[i] = true;//标记为访问过
    25                     flag[i]++;//入队列次数+1
    26                     if(flag[i]>=n)//若此点进入队列此数超过n次  说明有负环
    27                         return true;//有负环
    28                     que.push(i);//将 此点放入队列
    29                 }
    30             }
    31         }
    32     }
    33     return false;//没有负环
    34 }

        SPFA算法对于稀疏图才能发挥它的大作用,对于稀疏图我们用到的数据结构为  前向星

        下面就是 SPFA+前向星的程序  并应用了SLF  双向队列进行优化

        

     1 #include <cstdio>
     2 #include <cstring>
     3 #include <algorithm>
     4 #include <iostream>
     5 #include <queue>
     6 #define MAX 9999999
     7 
     8 using namespace std;
     9 
    10 struct node//前向星
    11 {
    12     int v,w;//v 终点,w 权值
    13     int next;//下一个
    14 };
    15 node edge[5203];//前向星
    16 int head[503];//头指针式的数组
    17 int cnt;//下标
    18 int n;//点的个数
    19 
    20 void add(int u, int v, int w)//加边  建议 若看不懂前向星 请自己动手画一下 按照给出的数据和程序
    21 {
    22     edge[cnt].v = v;//
    23     edge[cnt].w = w;//
    24     edge[cnt].next = head[u];//
    25     head[u] = cnt++;//
    26 }
    27 
    28 bool SPFA()
    29 {
    30     int i, u, v;//u 从Q中取出的点  v找到的点
    31     int dis[503];//保存最短路径
    32     int flag[503];//保存某点加入队列的次数
    33     bool vis[503];//标记数组
    34     deque<int>que;//双向队列  自己百度
    35 
    36     fill(dis,dis+n+1,MAX);//初始化
    37     memset(flag,0,sizeof(flag));//初始化
    38     memset(vis,false,sizeof(vis));//初始化
    39     dis[1] = 0;// s为1
    40     que.push_back(1);//将s = 1 加入队列
    41     while(!que.empty()){//当队列不为空
    42         u = que.front();//从队列中取出一个数
    43         que.pop_front();//删除
    44         vis[u] = false;//标记为未访问
    45         for(i=head[u];i!=-1;i=edge[i].next)//对所有与该边相连的边进行查找
    46         {
    47             v = edge[i].v;//保存点 便于操作
    48             if(dis[v]>dis[u]+edge[i].w){//进行松弛操作
    49                 dis[v] = dis[u]+edge[i].w;//松弛成功
    50                 if(!vis[v]){//若该点未被标记
    51                     vis[v] = true;//标记为真
    52                     flag[v]++;//该点入队列次数++
    53                     if(flag[v]>=n)//若该点进入队列次数超过n次 说明有负环
    54                         return true;//返回有负环
    55                     //一下为SLF优化
    56                     if(!que.empty() && dis[v]<dis[que.front()])//若为队列不为空 && 队列第一个点的最短距离大于当前点的最短距离
    57                         que.push_front(v);//将该点放到队首
    58                     else//不然
    59                         que.push_back(v);//放入队尾
    60                 }
    61             }
    62         }
    63     }
    64     return false;//没有负环
    65 }
    66 
    67 int main()
    68 {
    69     int u, v, w, m, k, t;
    70     
    71     scanf("%d",&t);//获取测试数据
    72     while(t--){
    73         memset(head,-1,sizeof(head));//初始化
    74         cnt = 0;//下标为0  初始化
    75         scanf("%d %d %d",&n,&m,&k);//获取点的个数 ,正边的个数, 负边的个数
    76         while(m--){
    77             scanf("%d %d %d",&u,&v,&w);//正边获取
    78             add(u,v,w);//无向图
    79             add(v,u,w);//双向建图
    80         }
    81         while(k--){
    82             scanf("%d %d %d",&u,&v,&w);
    83             add(u,v,-w);//单向图
    84         }
    85         printf("%s
    ",SPFA()?"YES":"NO");//输出结果
    86     }
    87     return 0;
    88 }
    题解

        所用数据结构有 前向星  双向队列

     1 struct node//前向星
     2 {
     3     int v,w;//v 终点,w 权值
     4     int next;//下一个
     5 };
     6 node edge[5203];//前向星
     7 int head[503];//头指针式的数组
     8 int cnt;//下标
     9 
    10  deque<int>que;//双向队列  自己百度

        主函数 对数据的获取 和图的建立

     1 int main()
     2 {
     3     int u, v, w, m, k, t;
     4     
     5     scanf("%d",&t);//获取测试数据
     6     while(t--){
     7         memset(head,-1,sizeof(head));//初始化
     8         cnt = 0;//下标为0  初始化
     9         scanf("%d %d %d",&n,&m,&k);//获取点的个数 ,正边的个数, 负边的个数
    10         while(m--){
    11             scanf("%d %d %d",&u,&v,&w);//正边获取
    12             add(u,v,w);//无向图
    13             add(v,u,w);//双向建图
    14         }
    15         while(k--){
    16             scanf("%d %d %d",&u,&v,&w);
    17             add(u,v,-w);//单向图
    18         }
    19         printf("%s
    ",SPFA()?"YES":"NO");//输出结果
    20     }
    21     return 0;
    22 }

        SPFA+前向星  

     1 bool SPFA()
     2 {
     3     int i, u, v;//u 从Q中取出的点  v找到的点
     4     int dis[503];//保存最短路径
     5     int flag[503];//保存某点加入队列的次数
     6     bool vis[503];//标记数组
     7     deque<int>que;//双向队列  自己百度
     8 
     9     fill(dis,dis+n+1,MAX);//初始化
    10     memset(flag,0,sizeof(flag));//初始化
    11     memset(vis,false,sizeof(vis));//初始化
    12     dis[1] = 0;// s为1
    13     que.push_back(1);//将s = 1 加入队列
    14     while(!que.empty()){//当队列不为空
    15         u = que.front();//从队列中取出一个数
    16         que.pop_front();//删除
    17         vis[u] = false;//标记为未访问
    18         for(i=head[u];i!=-1;i=edge[i].next)//对所有与该边相连的边进行查找
    19         {
    20             v = edge[i].v;//保存点 便于操作
    21             if(dis[v]>dis[u]+edge[i].w){//进行松弛操作
    22                 dis[v] = dis[u]+edge[i].w;//松弛成功
    23                 if(!vis[v]){//若该点未被标记
    24                     vis[v] = true;//标记为真
    25                     flag[v]++;//该点入队列次数++
    26                     if(flag[v]>=n)//若该点进入队列次数超过n次 说明有负环
    27                         return true;//返回有负环
    28                     //一下为SLF优化
    29                     if(!que.empty() && dis[v]<dis[que.front()])//若为队列不为空 && 队列第一个点的最短距离大于当前点的最短距离
    30                         que.push_front(v);//将该点放到队首
    31                     else//不然
    32                         que.push_back(v);//放入队尾
    33                 }
    34             }
    35         }
    36     }
    37     return false;//没有负环
    38 }

        好了,四种算法已经讲完。 

       

    5、 BFS 求解最短路

        在我们学习图的基本算法BFS 和DFS的时候,其实那就是一个求解每一步的权重都为1的最短路,那么权重不为1的情况我,我们是否继续使用呢?

        答案是肯定的。

        采用邻接表或前向星进行图的存储 , 则BFS的时间复杂度为   开始的初始化O(V)+BFS操作O(E) = O (V+E)

        继续以hdu 的  畅通工程续  为例

        

     1 #include <algorithm>
     2 #include <iostream>
     3 #include <cstdio>
     4 #include <cstring>
     5 #include <queue>
     6 
     7 using namespace std;
     8 
     9 struct P
    10 {
    11     int v, w;//v 顶点 w 最短距离
    12     bool operator <(const P &a)const{
    13         return a.w < w;//按w  从小到大排序
    14     }
    15 };
    16 struct node//前向星
    17 {
    18     int v, w;//v 顶点  w权重
    19     int next;//下一个位置
    20 };
    21 node edge[2003];
    22 int head[203];//头指针数组
    23 int cnt, s, t;// cnt 下标
    24 
    25 void add(int u, int v, int w)//加边操作
    26 {
    27     edge[cnt].v = v;
    28     edge[cnt].w = w;
    29     edge[cnt].next = head[u];
    30     head[u] = cnt++;
    31 }
    32 
    33 void BFS()
    34 {
    35     priority_queue<P>que;//优先队列   按w从小到大
    36     bool vis[203];//标记数组, 标记是否被访问过
    37     P p, q;
    38     int i, v;
    39 
    40     memset(vis,false,sizeof(vis));//初始化
    41     p.v = s;//顶点为 s
    42     p.w = 0;//距离为 0
    43     que.push(p);//放入队列
    44     while(que.empty() == false){//队列不为空
    45         p = que.top();//取出队列的队首
    46         que.pop();//删除
    47         if(p.v == t){//若找到终点
    48             printf("%d
    ",p.w);//输出结果
    49             return ;//返回
    50         }
    51         vis[p.v] = true;//此点标记为访问过
    52         for(i=head[p.v];i!=-1;i=edge[i].next)//查找与该点相连的点
    53         {
    54             v = edge[i].v;
    55             if(vis[v] == false){//若点未被访问过
    56                 q.v = v;//存入结构体
    57                 q.w = p.w+edge[i].w;//距离更新
    58                 que.push(q);//放入队列
    59             }
    60         }
    61     }
    62     printf("-1
    ");//若没有到达终点  输出-1
    63 }
    64 
    65 int main()
    66 {
    67     int m, u, v, w, n;
    68 
    69     while(scanf("%d %d",&n,&m)==2){//获取点的个数  边的个数
    70         memset(head,-1,sizeof(head));//初始化
    71         cnt = 0;//初始化
    72         while(m--){
    73             scanf("%d %d %d",&u,&v,&w);//获取u,v,w
    74             add(u,v,w);//加边
    75             add(v,u,w);//无向图   双向加边
    76         }
    77         scanf("%d %d",&s,&t);//获取起止点
    78         BFS();
    79     }
    80     return 0;
    81 }
    题解

        所用数据结构   前向星  优先队列

     1 struct P
     2 {
     3     int v, w;//v 顶点 w 最短距离
     4     bool operator <(const P &a)const{
     5         return a.w < w;//按w  从小到大排序
     6     }
     7 };
     8 priority_queue<P>que;//优先队列   按w从小到大
     9 struct node//前向星
    10 {
    11     int v, w;//v 顶点  w权重
    12     int next;//下一个位置
    13 };
    14 node edge[2003];
    15 int head[203];//头指针数组

        主函数对数据的获取

     1 int main()
     2 {
     3     int m, u, v, w, n;
     4 
     5     while(scanf("%d %d",&n,&m)==2){//获取点的个数  边的个数
     6         memset(head,-1,sizeof(head));//初始化
     7         cnt = 0;//初始化
     8         while(m--){
     9             scanf("%d %d %d",&u,&v,&w);//获取u,v,w
    10             add(u,v,w);//加边
    11             add(v,u,w);//无向图   双向加边
    12         }
    13         scanf("%d %d",&s,&t);//获取起止点
    14         BFS();
    15     }
    16     return 0;
    17 }

        加边操作

    1 void add(int u, int v, int w)//加边操作
    2 {
    3     edge[cnt].v = v;
    4     edge[cnt].w = w;
    5     edge[cnt].next = head[u];
    6     head[u] = cnt++;
    7 }

        BFS

     1 void BFS()
     2 {
     3     priority_queue<P>que;//优先队列   按w从小到大
     4     bool vis[203];//标记数组, 标记是否被访问过
     5     P p, q;
     6     int i, v;
     7 
     8     memset(vis,false,sizeof(vis));//初始化
     9     p.v = s;//顶点为 s
    10     p.w = 0;//距离为 0
    11     que.push(p);//放入队列
    12     while(que.empty() == false){//队列不为空
    13         p = que.top();//取出队列的队首
    14         que.pop();//删除
    15         if(p.v == t){//若找到终点
    16             printf("%d
    ",p.w);//输出结果
    17             return ;//返回
    18         }
    19         vis[p.v] = true;//此点标记为访问过
    20         for(i=head[p.v];i!=-1;i=edge[i].next)//查找与该点相连的点
    21         {
    22             v = edge[i].v;
    23             if(vis[v] == false){//若点未被访问过
    24                 q.v = v;//存入结构体
    25                 q.w = p.w+edge[i].w;//距离更新
    26                 que.push(q);//放入队列
    27             }
    28         }
    29     }
    30     printf("-1
    ");//若没有到达终点  输出-1
    31 }

    6、 路径还原  

        路径还原我们一般用不到,但是一般用不到,我们既然学了那么多最短路的算法,会还原一下,那不是锦上添花吗?

        所以 学!!!

        路径还原问题   在线题库中我没有发现。 这里就给出一组测试数据然后给出算法 就结束了。

        在此 用Dijkstra算法 演示路径还原 其他的bellman-ford算法 和floyd-Warshall算法都可用类似方法进行最短路的还原。

        第1行 两个数 n 和 m

        第2~m+1行  给出 u,v,w

        第m+2 行  给出两个数 s 和 t  

        求出 s—>t 的最短路  和 路径 

        前提 这个图是连通的。s—>t 的最短路是存在的。

        输入 :

        3 3

        0 1 1

        0 2 3

        1 2 1

        0 2

        输出:

        2(最短路)

        2—>1—>0(路径)

        在本文开始说松弛操作的时候  就说过在《算法导论》中,松弛操作还有一个记录路径的操作就 是这个了。

        

    1 if(dis[i]>dis[k]+G[k][i]){//在松弛操作中
    2     dis[i] = dis[k]+G[k][i];//不仅更新距离
    3     pre[i] = k;//同时记录路径
    4 }

        是这个样子。

        下面给出程序 :

     1 #include <algorithm>
     2 #include <iostream>
     3 #include <cstdio>
     4 #include <cstring>
     5 #include <queue>
     6 #define INF 999999
     7 using namespace std;
     8 
     9 int G[203][203];
    10 int n, s, t;
    11 
    12 bool Dijkstra()
    13 {
    14     int i, j, k;
    15     int dis[203];
    16     bool vis[203];
    17     int pre[203];//记录路径
    18 
    19     memset(vis,false,sizeof(vis));
    20     fill(dis,dis+n,INF);
    21     memset(pre,-1,sizeof(pre));//初始化为-1
    22     dis[s] = 0;
    23     for(i=0;i<n;i++)
    24     {
    25         k = -1;
    26         for(j=0;j<n;j++)
    27             if(vis[j] == false && (k == -1 || dis[k]>dis[j]))
    28                 k = j;
    29         if(k == -1)
    30             break;
    31         vis[k] = true;
    32         for(j=0;j<n;j++)
    33             if(vis[j] == false && dis[j]>dis[k]+G[k][j]){
    34                 dis[j] = dis[k]+G[k][j];
    35                 pre[j] = k;//在松弛操作中更新边的同时  记录路径
    36             }
    37     }
    38     printf("s——>t  的最短路为 :");
    39     printf("%d
    ",dis[t]);
    40     printf("路径为 :");
    41     //一下部分为路径的还原
    42     queue<int >que;//申请一个队列
    43     for(t;t!=-1;t=pre[t])//从t 一直寻找到s
    44         que.push(t);//放入队列
    45     printf("%d",que.front());//输出第一个
    46     que.pop();//删除
    47     while(!que.empty()){//队列不为空
    48         printf("——>%d",que.front());//输出
    49         que.pop();//删除
    50     }
    51 }
    52 
    53 int main()
    54 {
    55     int i, j, u, v, w, m;
    56 
    57     while(scanf("%d %d",&n,&m)==2){
    58         for(i=0;i<n;i++)
    59             for(j=0;j<n;j++)
    60                 G[i][j] = i==j?0:INF;
    61         while(m--){
    62             scanf("%d %d %d",&u,&v,&w);
    63             if(G[u][v]>w)
    64                 G[u][v] = G[v][u] = w;
    65         }
    66         scanf("%d %d",&s,&t);
    67         Dijkstra();
    68     }
    69     return 0;
    70 }

        刚刚开始一时兴起想写这篇文章,但自己没什么坚持力,自己磨磨蹭蹭的写了好几天才写完,今天终于完工了。    

        

        全文完。

     参考资料  :  1 《算法导论》 第三版

           2 《挑战程序设计》第2版

           3  百度百科

           4  维基百科


    作者:blueppo
    出处:http://www.cnblogs.com/Yan-C/
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

  • 相关阅读:
    ServletConfig对象
    乱码问题
    response request
    mysql
    数据库三范式 简单理解
    会话 cookie
    ServletContext对象
    读取工程中的配置文件
    Servlet与缓存
    C#捕获异常崩溃时
  • 原文地址:https://www.cnblogs.com/Yan-C/p/3916281.html
Copyright © 2011-2022 走看看