最短路径——贝尔曼-福特BelLnan-Ford算法详解与实现

7.5.2 弧上权值为任意值的单源点最短路径问题

弧上权值为任意值的单源点最短路径难点

  1. 图一般的情况:带权有向图G上弧的权值可能为负值。对于带权有向图来说,利用上节给出的迪克斯特拉算法,不一定能得到正确的结果(带负权的回路最短路径是不存在的,可以一直循环下去)。若设源点V0=A,使用迪克斯特拉算法,显然,结果是有问题的。
    在这里插入图片描述
  2. 为了能够求解弧上带有负权值的单源最短路径问题,贝尔曼(BelLnam)和福特(Ford)提出了从源点逐次经过其他顶点,以缩短到达终点的最短路径长度的方法。该方法有一个限制条件,即要求图中不能有路径长度为负值的回路。例如,图(b)中有一个回路(A,B,A),其路径长度为-5。当路径为(A,B,A,B,…,A,B,C)时,路径长度会越来越小,顶点A到顶点C的最短路径长度可达负无穷大。为了能够用有限条弧构成最短路径,必须把这种情况避开。所以在贝尔曼-福特算法中不考虑这种情况。
  3. 当图中没有路径长度为负值的回路时,有n个顶点的图中任意两个顶点之间如果存在最短路径,此路径最多有n-1条弧。这是因为如果路径上的弧数超过了n-1条时,必然会重复经过一个顶点,形成路径长度为负值回路。这就违反了先前的限制。下面将以此为依据考虑计算从源点v0到其他顶点u的最短路径的长度dist[u]。

贝尔曼-福特算法实现过程

算法思路

  1. 在贝尔曼-福特算法中,构造一个最短路径长度的数组序列:dist1 [],dist2 [],…,distn-1 []。其中,dist1 [u]表示从源点v0直接到终点u的最短路径的长度,即dist1 [u]=ars[v0][u];而 dist2 [u]表示从源点v0出发最多经过两条弧(一个中间顶点)到达终点u的最短路径的长度……distk [u]是从源点v0出发最多经过不构成带负长度回路的k条弧(k-1个中间顶点)到达终点u的最短路径的长度。算法的结果就是计算出distn-1 [u]。
  2. 可以用递推方式计算distn-1 []。设已经求出distk-1 [i],i=0,1,2,…,n-1。此即从源点v0出发最多经过不构成带负长度回路的k-1条到达终点i的最短路径的长度。从图的邻接矩阵中可以找到从任一顶点i直接到达另一顶点u的距离Arc[j][u],计算min{distk-1[i]+arcs[i][u]},用它与distk-1[u]比较,取小者作为distk[u]的值,就可得到从源点v0出发最多经过k条弧(k-1个中间顶点)构成不带负长度回路而到达终点u的最短路径的长度。因此,可得递推公式:
    dist1[u]=arcs[v0][u];
    distk[u]=min{distk-1[u],distk-1[i]+arcs[i][u]}
  3. 下面给出计算带权有向图的最短路径长度的贝尔曼-福特算法。算法中为了迭代地求distk[].使用了一个辅助数组distTemp[]来存放一次选代过程中dist[]的中间结果。一次迭代开始时先把dist[]复制到distTemp[],迭代过程中把中间结果记录在distTemp[]中(中间结果不能放在dist[]中),一次迭代结束后再把distTemp[]复制回 dist[].算法结束时dist[]中存放的值等于distn-1[]。
    在这里插入图片描述

代码实现

#include "AdjListDirNetwork.h"
///过程函数
template<class ElemType,class WeightType> void
ShortestPathBelLnamFord(const AdjListDirNetwork<ElemType,WeightType> &g,int v0,int *path,WeightType *dist)
{
    WeightType *distTemp,minVal,infinity=g.GetInfinity();
    int v,u,vexNum=g.GetVexNum();
    distTemp=new WeightType[vexNum];
    for(v=0;v<vexNum;v++)//初始化path和dist
    {
        dist[v]=(v0!=v)?g.GetWeight(v0,v):0;
        if(dist[v]==infinity)
            path[v]=-1;
        else
            path[v]=v0;
    }
    for(int k=2;k<vexNum;k++)
    {
    for(v=0;v<vexNum;v++)//复制辅助数组
        distTemp[v]=dist[v];
        for(u=0;u<vexNum;u++)//终点
        {
            if(u!=v0)
                for(v=0;v<vexNum;v++)//遍历每个顶点
                    if(v!=v0 && distTemp[u]>dist[v]+g.GetWeight(v,u))
                    {
                        distTemp[u]=dist[v]+g.GetWeight(v,u);
                        path[u]=v;
                    }
        }
        for(v=0;v<vexNum;v++)
            dist[v]=distTemp[v];
    }
}
///最终实现函数
template<class ElemType,class WeightType> void
BelLnamFord(const AdjListDirNetwork<ElemType,WeightType> &g)
{
    int n=g.GetVexNum();
    int path[n];
    WeightType dist[n];
    ShortestPathBelLnamFord(g,0,path,dist);
}

效率分析

  1. 对于有n个顶点和e条边的有向网络,在上述算法中,有两个并列for循环。第一个for循环进行数组dist[]的初始化,其时间复杂度为O(n)。第二for循环是三重嵌套的,如果使用邻接矩阵作为图的存储表示,其时间复杂度为O(n3);如果使用逆邻接表表示,最内层的for循环改为while循环,可使算法的复杂度降为O(n2+n*e)
  2. 还可以有一些其他的方法改进算法的时间复杂度,例如,考虑在三重嵌套循环执行过程中监视dist[]数组的变化。假设在一次循环中dist[]数组没有发生改变,那么在以后的循环中它也不会改变,此时可以结束算法

思考

  1. 其优于Dijkstra算法的方面是边的权值可以为负数、实现简单;缺点是时间复杂度过高,高达O(VE)。但算法可以进行若干种优化,提高了效率。
  2. 此算法运用松弛技术,对每个顶点v属于V,逐步减小从源点s到v的d[v],直至其达到实际最短路径的权d(s,v)。
  3. 负边权操作
    与迪科斯彻算法不同的是,迪科斯彻算法的基本操作“拓展”是在深度上寻路,而“松弛”操作则是在广度上寻路,这就确定了贝尔曼-福特算法可以对负边进行操作而不会影响结果。
  4. 负权环判定
    因为负权环可以无限制的降低总花费,所以如果发现第n次操作仍可降低花销,就一定存在负权环。

优化方法:

  1. 循环的提前跳出
    在实际操作中,贝尔曼-福特算法经常会在未达到 |V|-1 次前就出解,|V|-1 其实是最大值。于是可以在循环中设置判定,在某次循环不再进行松弛时,直接退出循环,进行负权环判定。
  2. 队列优化(来自百度百科)
    西南交通大学的段凡丁于1994年提出了用队列来优化的算法。松弛操作必定只会发生在最短路径前导节点松弛成功过的节点上,用一个队列记录松弛过的节点,可以避免了冗余计算。原文中提出该算法的复杂度为,是个比较小的系数,但该结论未得到广泛认可。
 2     queue<int> q;
 3     bool inq[maxn] = {false};
 4     for(int i = 1; i <= N; i++) dis[i] = 2147483647;
 5     dis[s] = 0;
 6     q.push(s); inq[s] = true;
 7     while(!q.empty()) {
 8         int x = q.front(); q.pop();
 9         inq[x] = false;
10         for(int i = front[x]; i !=0 ; i = e[i].next) {
11             int k = e[i].v;
12             if(dis[k] > dis[x] + e[i].w) {
13                 dis[k] = dis[x] + e[i].w;
14                 if(!inq[k]) {
15                     inq[k] = true;
16                     q.push(k);
17                 }
18             }
19         }
20     }
21     for(int i =  1; i <= N; i++) cout << dis[i] << ' ';
22     cout << endl;
23     return 0;
24 }
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值