Bellman-Ford 算法及其优化

一、Bellman-Ford算法思想
       Bellman-Ford算法能在更普遍的情况下(存在负权边)解决单源点最短路径问题。对于给定的带权(有向或无向)图 G=(V,E),其源点为s,加权函数 w是 边集 E 的映射。对图G运行Bellman-Ford算法的结果是一个布尔值,表明图中是否存在着一个从源点s可达的负权回路。若不存在这样的回路,算法将给出从源点s到 图G的任意顶点v的最短路径d[v]。
Bellman-Ford算法流程分为三个阶段:
(1)    初始化:将除源点外的所有顶点的最短距离估计值 d[v] ←+∞, d[s] ←0;
(2)    迭代求解:反复对边集E中的每条边进行松弛操作,使得顶点集V中的每个顶点v的最短距离估计值逐步逼近其最短距离;(运行|v|-1次)
(3)    检验负权回路:判断边集E中的每一条边的两个端点是否收敛。如果存在未收敛的顶点,则算法返回false,表明问题无解;否则算法返回true,并且从源点可达的顶点v的最短距离保存在 d[v]中。

算法描述如下:

Bellman-Ford(G,w,s) :boolean   //图G ,边集 函数 w ,s为源点
       for each vertex v ∈ V(G) do        //初始化 1阶段
           d[v] ←+∞
       d[s] ←0;                             //1阶段结束
       for i=1 to |v|-1 do               //2阶段开始,双重循环。
          for each edge(u,v) ∈E(G) do //边集数组要用到,穷举每条边。
             If d[v]> d[u]+ w(u,v) then      //松弛判断
                d[v]=d[u]+w(u,v)               //松弛操作   2阶段结束
       for each edge(u,v) ∈E(G) do
           If d[v]> d[u]+ w(u,v) then
            Exit false
    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可达的顶点必将求出最短距离。如果 d[v]仍保持 +∞,则表明从s到v不可达。 如果有负权回路,那么第 |v|-1 遍松弛操作仍然会成功,这时,负权回路上的顶点不会收敛。 例如对于上图,边上方框中的数字代表权值,顶点A,B,C之间存在负权回路。S是源点,顶点中数字表示运行Bellman-Ford算法后各点的最短距离估计值。 此时d[a]的值为1,大于d[c]+w(c,a)的值-2,由此d[a]可以松弛为-2,然后d[b]又可以松弛为-5,d[c]又可以松弛为-7.下一个周期,d[a]又可以更新为更小的值,这个过程永远不会终止。因此,在迭代求解最短路径阶段结束后,可以通过检验边集E的每条边(u,v)是否满足关系式 d[v]> d[u]+ w(u,v) 来判断是否存在负权回路。

二、基本算法之上的优化。

       分析 Bellman-Ford算法,不难看出,外层循环(迭代次数)|v|-1实际上取得是上限。由上面对算法正确性的证明可知,需要的迭代遍数等于最短路径树的高度。如果不存在负权回路,平均情况下的最短路径树的高度应该远远小于 |v|-1,在此情况下,多余最短路径树高的迭代遍数就是时间上的浪费,由此,可以依次来实施优化。 从细节上分析,如果在某一遍迭代中,算法描述中第7行的松弛操作未执行,说明该遍迭代所有的边都没有被松弛。可以证明(怎么证明?):至此后,边集中所有的边都不需要再被松弛,从而可以提前结束迭代过程。这样,优化的措施就非常简单了。 设定一个布尔型标志变量flag,初值为false。在内层循环中,仅当有边被成功松弛时,将 flag 设置为true。如果没有边被松弛,则提前结束外层循环。这一改进可以极大的减少外层循环的迭代次数。
优化后的 bellman-ford函数如下。

function bellmanford(s:longint):boolean;
     begin
        for i:=1 to nv do
          d[i]:=max;
        d[s]:=0;
        for i:=1 to nv-1 do
         begin
         flag:=false;
          for j:=1 TO ne do
          if(d[edges[j].s]<>max) and (d[edges[j].e]>d[edges[j].s]+edges[j].w)
               then begin
d[edges[j].e]:=d[edges[j].s]+edges[j].w ;
flag:=true;
                         end;
                if not flag then break;
end;
        for i:=1 to ne do
          if d[edges[j].e]>d[edges[j].s]+edges[j].w then exit(false);
        exit(true);
     end;

C++代码:

struct Edge  
{  
    int s,e; 
    int cost;  
};  
Edge edge[MAX];  
int map[505][505];  
int n,m,w;  
int dist[MAX];  
int edge_num;     
bool bellman_ford()  
{  
    int s,e,w,i,j;  
    bool flag;  
	for(i=1;i<=n;i++)
        dist[i] = INF;     
    dist[1]=0;     
	for(i=1;i<=n;i++) //顶点下标从1开始,进行dist数组更新操作
    {  
        flag=false;  
        for (j=0;jdist[s]+w)  //松弛迭代过程
            {  
                dist[e]=dist[s]+w;  
                flag=true; 
            }  
        }  
        if(!flag) 
			break; 
    }    
}


这样看似平凡的优化,会有怎样的效果呢?有研究表明,对于随机生成数据的平均情况,时间复杂度的估算公式为
1.13|E|                    if |E|<|V|
0.95*|E|*lg|V|              if |E|>|V|
优化后的算法在处理有负权回路的测试数据时,由于每次都会有边被松弛,所以flag每次都会被置为true,因而不可能提前终止外层循环。这对应了最坏情况,其时间复杂度仍旧为O(VE)。
优化后的算法的时间复杂度已经和用二叉堆优化的Dijkstra算法相近了,而编码的复杂程度远比后者低。加之Bellman-Ford算法能处理各种边值权情况下的最短路径问题,因而还是非常优秀的。
三、SPFA 算法
       SPFA是目前相当优秀的求最短路径的算法,值得我们掌握。
       SPFA对Bellman-Ford算法优化的关键之处在于意识到:只有那些在前一遍松弛中改变了距离估计值的点,才可能引起他们的邻接点的距离估计值的改变。因此,用一个先进先出的队列来存放被成功松弛的顶点。初始时,源点s入队。当队列不为空时,取出对首顶点,对它的邻接点进行松弛。如果某个邻接点松弛成功,且该邻接点不在队列中,则将其入队。经过有限次的松弛操作后,队列将为空,算法结束。SPFA算法的实现,需要用到一个先进先出的队列 queue 和一个指示顶点是否在队列中的 标记数组 mark。为了方便查找某个顶点的邻接点,图采用临界表存储。
   需要注意的是:仅当图不存在负权回路时,SPFA能正常工作。如果图存在负权回路,由于负权回路上的顶点无法收敛,总有顶点在入队和出队往返,队列无法为空,这种情况下SPFA无法正常结束。
判断负权回路的方案很多,世间流传最广的是记录每个结点进队次数,超过|V|次表示有负权
还有一种方法为记录这个结点在路径中处于的位置,ord[i],每次更新的时候ord[i]=ord[x]+1,若超过|V|则表示有负圈.....
其他方法还有很多,我反倒觉得流传最广的方法是最慢的.......
关于SPFA的时间复杂度,不好准确估计,一般认为是 O(kE),k是常数
四、结论
       经过优化Bellman-Ford算法是非常优化的求单源最短路径的算法,SPFA时间效率要优于第一种优化形式,但第一种优化形式的编码复杂度低于SPFA。两种优化形式的编程复杂度都低于Dijkstra算法。如果在判断是否存在负权回路,推荐使用第一种优化形式,否则推荐使用SPFA

你可能感兴趣的:(算法)