Bellman-Ford算法学习笔记【最短路径(负权边)】

       通过对于Djkstra的学习,大致了解了最短路径问题,但是Djsktra算法由于其操作过程上的不足,不能处理带有负权边的情况,这个算法,Bellman-Ford【这些名字应该怎么记。。】则可以处理带有负权的问题,其中也用到了“松弛”操作,是根据边的长度来确定的,与之前的松弛有些不同,之前或者按点,或者按边的顺序进行遍历,这个是任选一条边为起点,然后对边集进行遍历选择第二条边,再将边集遍历一遍,有些类似于prim算法选择最小生成树是对点进行的操作,但还是有不同,,两三句话讲不明白,这几个算法总感觉有点像,有点懵,把最短路结束之后要好好区分一下。。

学习资料:《啊哈!算法》相关内容【安利安利】。

处理问题:

       可以处理带负权的单源最短路问题。【有向图无向图皆宜】

算法思想:

       用dis数组表述源点到各个顶点的最短路径,初始时dis[源点]=0,其余最短距离皆为正无穷,用数组 u,v,w,记录边的信息,例如:第 i 条边存储在 u[ i ] ,v[ i ] ,w[ i ]中,表示顶点 u[ i ] 到顶点 v[ i ],这条边的权值为 w[ i ]【这几个数组之间的关系一定要搞明白,各个节点有编号,在dis[节点编号 ] 中存储的是从源点到该节点的最短距离;各个边也有编号,u[ i ] ,v[ i ] 内部存储边 i 的相连的两端点的编号,w[ i ] 中表示该边的权值,一定要记清楚这个,核心代码围着这几个数组转】;

if(dis[v[i]]>dis[u[i]]+w[i])
    dis[v[[i]]=dis[u[i]]+w[i];

       看看上面这两句代码,表b示:看看能否通过 u[ i ] ->v[ i ] (权值存储在w[ i ] 中) ,使得1 号顶点到 v [ i ] 号顶点的距离变短;即1号顶点到 u[ i ] 号顶点的距离(dis[ u[ i ] )加上 u [ i ]->v[ i ] 这条边(权值为 w[ i ]) 的值是否会比原先1号顶点到v[j号顶点的距离(dis[v[]]) 要小。这一点与Dijkstra的“松弛”操作是一样的。如果我们要把所有的边都松弛一遍,则需要循环m次【边数】代码:

for(i=1;i<=m; i++)
    if( dis[v[i]] > dis[u[i]]+ w[i] )
        dis[v[i]]= dis[u[i]] + w[i] ;

        把每一.条边都“松弛”一遍后,将会有什么效果呢? 现在来举个具体的例子。求下图1号顶点到其余所有顶点的最短路径。

                        《Bellman-Ford算法学习笔记【最短路径(负权边)】》

         依然用一个dis数组来存储1号顶点到所有顶点的距离。

                       《Bellman-Ford算法学习笔记【最短路径(负权边)】》

        上方右图中每个顶点旁的值【带下划线的数字】为源点【现在为1号点】到该顶点的最短路“估计值”【距离】,即数组dis中对应的值。根据边给出的顺序,先来处理第1条边 “2  3  2 ” 【2号节点到3号节点权值为2的边,通过这条边进行松弛】,即判断dis[3]是否大于dis[2]+2。 此时dis[3]是∞, dis[2]是∞,因此dis[2]+2也是∞,所以通过“ 2 3 2 ”这条边不能使dis[3]的值变小,松池失败。
        同理,继续处理第2条边“1 2 -3”【1号节点到8号节点权值为-3的边】,现在有dis[2]大于dis[1]+(-3),通过这条边可以使dis[2]的值从∞变为-3,因此松驰成功。用同样的方法处理剩下的每一.条边。对所有的边松弛一遍后的结果如下。

                       《Bellman-Ford算法学习笔记【最短路径(负权边)】》

       这是从任意一点开始,将所有的边松弛一遍的结果,可以看到,一轮过后使得dis[2]和dis[5]的值变小,即1号顶到2号顶点的距离和1号顶点到5号顶点的距离都变短了。这当然是不够的,显然还有别的边没有取到真正的路径值,接下来我们需要对所有的边再进行一轮松弛,操作过程与上一轮是一样的,看看会发生什么。

                     《Bellman-Ford算法学习笔记【最短路径(负权边)】》

      在第二轮的松弛中,可以看到,现在通过“2 3 2” 【2→3 距离为2】这条边,可以使1号顶点到3 号顶点的距离(dis[3]) 变短了。【还记不记得,这条边在第一轮也松弛过,但在上一轮松弛失败了,这一轮却成功了,神奇?不神奇,因为在第一轮的松弛过后,1 号顶点到2号顶点的距离(dis[2]) 已经发生了变化,这一轮再通过“2 3 2” 【2→3 距离为2】这条边进行松弛的时候,就可以使1号顶点到3号顶点的距离(dis[3]) 的值变小。

      换句话说,第1轮在对所有的边进行松弛之后,得到的是从1号顶点“只能经过一条边”到达其余各顶点的最短路径长度。第2轮在对所有的边进行松弛之后,得到的是从1号顶点“最多经过两条边”到达其余各顶点的最短路径长度。】如果进行k轮的话,得到的就是1号顶点“最多经过k条边”到达其余各顶点的最短路径长度。问题来了:需要进行多少轮?
       答案是:需要进行n-1轮。因为在一一个含有n个顶点的图中,任意两点之间的最短路径最多包含n-1边。【真的最多只能包含n-1条边?最短路径中不可能包含回路吗?恩,,不可能!最短路径肯定是一个不包含回路的简单路径。why?回路分为正权回路(即回路权值之和为正)和负权回路(即回路权值之和为负)。为什么这两种回路都不可能有?来讨论一下:如果最短路径中包含正权回路,那么去掉这个回路,一定可以得到更短的路径。如果最短路径中包含负权回路,那么肯定没有最短路径,因为每多走一次负权回路就可以得到更短的路径。因此,最短路径肯定是一个不包含回路的简单路径,即最多包含n-1条边,所以进行n-1轮松弛就可以了。】

      Ok,解决完这个为什么第一轮不可以但第二轮就可以的神奇边的问题,接着进行第3轮和第4轮松弛操作,这里只需进行4轮就可以了,因为这个图只有5个顶点。

                            《Bellman-Ford算法学习笔记【最短路径(负权边)】》

       有些特别特别爱思考的同学又会有一个疑问,这里貌似不用进行第4轮嘛,因为进行第4轮之后dis 数组没有发生任何变化,其实是最多进行n-1轮松弛。
       整个Bellman-Ford算法用-句话概括就是:对所有的边进行n-1次“松弛”操作。核心代码只有4行,如下。

for (k=1;k<=n-1;k++) //进行n-1轮松弛
      for(i=1;i<=m;i++) //枚举每一条边
      if( dis[v[i]] > dis[u[i]] + w[i]) //尝试对每一 条边进行松弛
      dis[v[i]]=dis[u[i]] + w[i];

       Ok,总结一下。因为最短路径上最多有n-1条边,因此Bellman-Ford算法最多有n-1 个阶段。在每-一个阶段,我们对每一条边都要执行松弛操作。其实每实施一次松弛操作, 就会有一些顶点已经求得其最短路,即这些顶点的最短路的“估计值”变为“确定值”。此后这些顶点的最短路的值就会–直保持不变,不再受后续松弛操作的影响(但是,每次还是会判断是否需要松弛,这里浪费了时间,是否可以优化呢? )。在前k个阶段结束后,就已经找出了从源点发出“最多经过k条边”到达各个顶点的最短路。直到进行完n-1个阶段后,便得出了最多经过n -1条边的最短路。

      Bellman-Ford算法的完整的代码如下:

#include <stdio.h>
#include <iostream>
#include <cstdio>
#include <bits/stdc++.h>
int main(){
    int dis[10] ,i,k,n,m,u[10],v[10],w[10] ;
    int inf=99999999;//用inf(infinity的缩写)存储一个我们认为的正无穷值

    //读入n和m, n表示顶点个数,m表示边的条数
    scanf("%d%d",&n,&m);

    //读入边
    for(i=1;i<=m;i++)
        scanf("%d %d %d",&u[i] ,&v[i],&w[i]);

    //初始化dis数组,这里是1号顶点到其余各个顶点的初始路程
    for(i=1;i<=n;i++)
        dis[i]=inf;
    dis[1]=0;

    //Bellman-Ford算法核心语句
    for(k=1;k<=n-1;k++)
        for(i=1;i<=m;i++)
            if( dis[v[i]]> dis[u[i]] + w[i] )
                dis[v[i]]=dis[u[i]]+ w[i] ;

    //输出最终的结果
    for(i=1;i<=n;i++)
        printf("%d ",dis[i]);

    getchar();
    getchar();
    return 0;
}

/*
可以輸入以下数据迸行驗正。
第一行两个整数 n m。
n表示頂点个数(頂点編号为l~N),m表示边的条数。
接下来m行表示,毎行有3个数xyz。
表示从頂点x到頂点y的边的权值为z。

5 5
2 3 2
1 2 -3
1 5 5
4 5 2
3 4 3

送行結果是:
0 -3 -12 4
*/

       此外,Bellman-Ford算法还可以检测一- 个图是否含有负权回路。如果在进行n-1轮松弛之后,仍然存在可满足:

if( dis[v[i]]> dis[u[i]]+ w[i]
    dis[v[i]] = dis[u[i]] + w[i];

的情况,也就是说在进行n-1轮松弛后,仍然可以继续成功松弛,那么此图必然存在负权回路。在之前的证明中我们已经讨论过,如果一个图如果没有负权回路,那么最短路径所包含的边最多为n-1条,即进行n-1轮松弛之后最短路不会再发生变化。如果在n-1轮松弛之后最短路仍然会发生变化,则该图必然存在负权回路,关键代码如下:

//Bellman Ford算法核心语句
for(k=1;k<=n-1;k++)
    for(i=1;i<=m;i++)
        if( dis[v[i]] > dis[u[i]]+ w[i]) flag=1;
             dis[v[i]] = dis[u[i]]+ w[i] ;
//检测负权回路
flag=0; 
for(i=1;i<=m;i++)
    if( dis[v[i]] > dis[u[i]] + w[i] )  flag=1;

if (flag==1) printf("此图含有负权回路");

       Bellman-Ford 算法的时间复杂度是O(NM),这个时间复杂度貌似比Djkstra 算法还要高,其实还可以对其进行优化。在实际操作中,Bellman- Ford算法经常会在未达到n- -1轮松弛前就已经计算出最短路,之前已经说过,n-1其实是最大值。因此可以添加一个一维数组用来备份数组dis。如果在新一轮的松弛中数组dis没有发生变化,则可以提前跳出循环,代码如下。

#include <stdio.h>
int main(){
    int dis[10],bak[10],i,k,n,m,u[10],v[10],w[10],check,flag;
    int inf=99999999;//用inf(infinity的缩写)存储一个我们认为的正无穷值

    //读入n和m, n表示顶点个数,m表示边的条数
    scanf("%d%d",&n,&m);

    //读入边
    for(i=1;i<=m;i++)
        scanf("%d %d %d",&u[i] ,&v[i],&w[i]);

    //初始化dis数组,这里是1号顶点到其余各个顶点的初始路程
    for(i=1;i<=n;i++)
        dis[i]=inf;
    dis[1]=0;

    //Bellman-Ford算法核心语句
    for(k=1;k<=n-1;k++) {
        //将dis数组备份至bak数组中
        for(i=1;i<=n;i++) bak[i]=dis[i];
        //进行一轮松弛
        for(i=1;i<=m;i++)
            if( dis[v[i]] > dis[u[i]]+ w[i] )
                dis[v[i]] = dis[u[i]] + w[i];
        //松弛完毕后检测dis数组是否有更新
        check=0;
        for(i=1;i<=n;i++) if( bak[i]!=dis[i] ) {check=1;break;}
        if(check==0) break; //如果dis数组没有更新,提前退出循环结束算法
    }
    //检测负权回路
    flag=0;
    for(i=1;i<=m;i++)
        if( dis[v[i]]>dis[u[i]]+w[i]) flag=1;

    if (flag==1) printf ("此图含有负权回路");
    else
    {
        //输出最终的结果
        for(i=1;i<=n;i++)
            printf("%d ",dis[i]);
    }
    getchar() ; getchar() ;
    return 0;
}

【Bellman-Ford算法的另外一种优化在文中已经有所提示:在每实施一次松弛操作后, 就会有一些顶点已经求得其最短路,此后这些顶点的最短路的估计值就会一直保持不变, 不再受后续松弛操作的影响,但是每次还要判断是否需要松弛,这里浪费了时间。这就启发我们:每次仅对最短路估计值发生变化了的顶点的所有出边执行松弛操作。Bellman-Ford还可以用队列优化,比较庆幸前两天将队列补了补】

Goodmorning,Goodnight~
 

    原文作者:Bellman - ford算法
    原文地址: https://blog.csdn.net/sodacoco/article/details/83865941
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞