Bellman-ford算法 学习笔记

       Bellman-Ford算法与Dijkstra算法思想一样,用于求解单源点最短路径问题。Bellman-ford算法除了可求解边权均非负的问题外,关键是还可以解决存在负权边的问题,而Dijkstra算法只能处理边权非负的问题,因此 Bellman-Ford算法的适用面要广泛一些。但是,原始的Bellman-Ford算法时间复杂度为 OVE,Dijkstra算法的时间复杂度高,就连经典的《算法导论》也只介绍了基本的Bellman-Ford算法,事实上,有多种形式的Bellman-Ford算法的优化实现。这些优化实现在时间效率上得到相当提升,例如近一两年被热捧的SPFAShortest-Path Faster Algoithm 更快的最短路径算法)算法的时间效率甚至由于Dijkstra算法,本文试图对Bellman-Ford算法做一个比较全面的介绍。

Bellman-Ford算法思想

    Bellman-Ford算法能在更普遍的情况下(存在负权边)解决单源点最短路径问题。对于给定的带权(有向或无向)图 G=V,E),其源点为s对图G运行Bellman-Ford算法的结果是一个布尔值,表明图中是否存在着一个从源点s可达的负权回路。若不存在这样的回路,算法将给出从源点s G的任意顶点v的最短路径dist[v]。


Bellman-Ford算法流程分为三个阶段:

(1)    初始化:将除源点外的所有顶点的最短距离估计值 dist[v] ←+∞, dist[s] ←0;

(2)    迭代求解:反复对边集E中的每条边进行松弛操作,使得顶点集V中的每个顶点v的最短距离估计值逐步逼近其最短距离;(运行|v|-1次)

(3)    检验负权回路:判断边集E中的每一条边的两个端点是否收敛。如果存在未收敛的顶点,则算法返回false,表明问题无解;否则算法返回true,并且从源点可达的顶点v的最短距离保存在 dist[v]中。


Bellman-Ford(G,w,s) boolean   //,边集 函数 w s为源点

1        for each vertex v ∈ V(G) do        //初始化 1阶段

2            dist[v] ←+∞

3        dist[s] ←0;                             //1阶段结束

4        for i=1 to |v|-1 do               //2阶段开始,双重循环。

5           for each edge(u,v) ∈E(G) do //边集数组要用到,穷举每条边。

6              If dist[v]> dist[u]+ w(u,v) then      //松弛判断

7                 dist[v]=dist[u]+w(u,v)               //松弛操作   2阶段结束

8        for each edge(u,v) ∈E(G) do

9            If dist[v]> dist[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可达的顶点必将求出最短距离。如果 dist[v]仍保持 +∞,则表明从s到v不可达。

如果有负权回路,那么第 |v|-1 遍松弛操作仍然会成功,这时,负权回路上的顶点不会收敛。

基本 Bellman-Ford 算法的c语言实现:


#include<stdio.h>

#include<stdlib.h>

#define MAX 0x3f3f3f3f

#define N 1010

int nodenum, edgenum, original; //点,边,起点

typedef struct Edge //边

{

    int u, v;

    int cost;

}Edge;

Edge edge[N];

int dist[N], pre[N];

bool Bellman_Ford()

{

    for(int i = 1; i <= nodenum; ++i) //初始化

        dis[i] =  MAX;

    dist[original] = 0;

    for(int i = 1; i <= nodenum – 1; ++i)

        for(int j = 1; j <= edgenum; ++j)

        {

            if(dis[edge[j].v] > dis[edge[j].u] + edge[j].cost) //松弛

            {

                dis[edge[j].v] = dis[edge[j].u] + edge[j].cost;

                pre[edge[j].v] = edge[j].u;

            }

        }

    bool flag = 1; //判断是否含有负权回路

    for(int i = 1; i <= edgenum; ++i)

        if(dis[edge[i].v] > dis[edge[i].u] + edge[i].cost)

        {

            flag = 0;

            break;

        }

    return flag;

}

void print_path(int root) //打印最短路的路径(反向)

{

    while(root != pre[root]) //前驱

    {    

        printf(“%d–>”, root);

        root = pre[root];

    }

    if(root == pre[root])

    printf(“%d\n”, root);

}

int main()

{

    scanf(“%d%d%d”, &nodenum, &edgenum, &original);

    pre[original] = original;

    for(int i = 1; i <= edgenum; ++i)

    {

        scanf(“%d%d%d”, &edge[i].u, &edge[i].v, &edge[i].cost);

    }

    if(Bellman_Ford())

        for(int i = 1; i <= nodenum; ++i) //每个点最短路

        {

            printf(“%d\n”, dis[i]);

            printf(“Path:”);

            print_path(i);

        }

    else

        printf(“have negative circle\n”);

    return 0;

}

基本算法之上的优化


    分析 Bellman-Ford算法,不难看出,外层循环(迭代次数)|v|-1实际上取得是上限。由上面对算法正确性的证明可知,需要的迭代遍数等于最短路径树的高度。如果不存在负权回路,平均情况下的最短路径树的高度应该远远小于 |v|-1,在此情况下,多余最短路径树高的迭代遍数就是时间上的浪费,由此,可以依次来实施优化。

从细节上分析,如果在某一遍迭代中,算法描述中第7行的松弛操作未执行,说明该遍迭代所有的边都没有被松弛。至此后,边集中所有的边都不需要再被松弛,从而可以提前结束迭代过程。这样,优化的措施就非常简单了。

设定一个布尔型标志变量 is_relaxed,初值为false。在内层循环中,仅当有边被成功松弛时,将 is_relaxed 设置为true。如果没有边被松弛,则提前结束外层循环。这一改进可以极大的减少外层循环的迭代次数。优化后的 bellman-ford函数如下。


function bellmanford(s:longint):boolean;

     begin

        for i:=1 to nv do

          dist[i]:=max;

        dist[s]:=0;

        for i:=1 to nv-1 do

         begin

         is_relaxed:=false;

          for j:=1 TO ne do

          if(dist[edges[j].s]<max) and (dist[edges[j].e]>dist[edges[j].s]+edges[j].w)

               then begin

dist[edges[j].e]:=dist[edges[j].s]+edges[j].w ;

is_relaxed:=true;

                         end;

                if not is_relaxed then break;

end;

        for i:=1 to ne do

          if dist[edges[j].e]>dist[edges[j].s]+edges[j].w then exit(false);

        exit(true);

     end;

优化后的算法在处理有负权回路的测试数据时,由于每次都会有边被松弛,所以relaxed每次都会被置为true,因而不可能提前终止外层循环。这对应了最坏情况,其时间复杂度仍旧为O(VE)

优化后的算法的时间复杂度已经和用二叉堆优化的Dijkstra算法相近了,而编码的复杂程度远比后者低。加之Bellman-Ford算法能处理各种边值权情况下的最短路径问题,因此还是很不错的。



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