7.5.2 弧上权值为任意值的单源点最短路径问题
弧上权值为任意值的单源点最短路径难点
- 图一般的情况:带权有向图G上弧的权值可能为负值。对于带权有向图来说,利用上节给出的迪克斯特拉算法,不一定能得到正确的结果(带负权的回路最短路径是不存在的,可以一直循环下去)。若设源点V0=A,使用迪克斯特拉算法,显然,结果是有问题的。
- 为了能够求解弧上带有负权值的单源最短路径问题,贝尔曼(BelLnam)和福特(Ford)提出了从源点逐次经过其他顶点,以缩短到达终点的最短路径长度的方法。该方法有一个限制条件,即要求图中不能有路径长度为负值的回路。例如,图(b)中有一个回路(A,B,A),其路径长度为-5。当路径为(A,B,A,B,…,A,B,C)时,路径长度会越来越小,顶点A到顶点C的最短路径长度可达负无穷大。为了能够用有限条弧构成最短路径,必须把这种情况避开。所以在贝尔曼-福特算法中不考虑这种情况。
- 当图中没有路径长度为负值的回路时,有n个顶点的图中任意两个顶点之间如果存在最短路径,此路径最多有n-1条弧。这是因为如果路径上的弧数超过了n-1条时,必然会重复经过一个顶点,形成路径长度为负值回路。这就违反了先前的限制。下面将以此为依据考虑计算从源点v0到其他顶点u的最短路径的长度dist[u]。
贝尔曼-福特算法实现过程
算法思路
- 在贝尔曼-福特算法中,构造一个最短路径长度的数组序列: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]。
- 可以用递推方式计算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]} - 下面给出计算带权有向图的最短路径长度的贝尔曼-福特算法。算法中为了迭代地求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++)
{
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);
}
效率分析
- 对于有n个顶点和e条边的有向网络,在上述算法中,有两个并列for循环。第一个for循环进行数组dist[]的初始化,其时间复杂度为O(n)。第二for循环是三重嵌套的,如果使用邻接矩阵作为图的存储表示,其时间复杂度为O(n3);如果使用逆邻接表表示,最内层的for循环改为while循环,可使算法的复杂度降为O(n2+n*e)。
- 还可以有一些其他的方法改进算法的时间复杂度,例如,考虑在三重嵌套循环执行过程中监视dist[]数组的变化。假设在一次循环中dist[]数组没有发生改变,那么在以后的循环中它也不会改变,此时可以结束算法。
思考
- 其优于Dijkstra算法的方面是边的权值可以为负数、实现简单;缺点是时间复杂度过高,高达O(VE)。但算法可以进行若干种优化,提高了效率。
- 此算法运用松弛技术,对每个顶点v属于V,逐步减小从源点s到v的d[v],直至其达到实际最短路径的权d(s,v)。
- 负边权操作
与迪科斯彻算法不同的是,迪科斯彻算法的基本操作“拓展”是在深度上寻路,而“松弛”操作则是在广度上寻路,这就确定了贝尔曼-福特算法可以对负边进行操作而不会影响结果。 - 负权环判定
因为负权环可以无限制的降低总花费,所以如果发现第n次操作仍可降低花销,就一定存在负权环。
优化方法:
- 循环的提前跳出
在实际操作中,贝尔曼-福特算法经常会在未达到 |V|-1 次前就出解,|V|-1 其实是最大值。于是可以在循环中设置判定,在某次循环不再进行松弛时,直接退出循环,进行负权环判定。 - 队列优化(来自百度百科)
西南交通大学的段凡丁于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 }