Bellman-Ford & SPFA 算法——求解单源点最短路径问题
Bellman-Ford算法与另一个非常著名的Dijkstra算法一样,用于求解单源点最短路径问题。Bellman-ford算法除了可求解边权均非负的问题外,还可以解决存在负权边的问题(意义是什么,好好思考),而Dijkstra算法只能处理边权非负的问题,因此 Bellman-Ford算法的适用面要广泛一些。但是,原始的Bellman-Ford算法时间复杂度为O(VE),比Dijkstra算法的时间复杂度高,所以常常被众多的大学算法教科书所忽略,就连经典的《算法导论》也只介绍了基本的Bellman-Ford算法,在国内常见的基本信息学奥赛教材中也均未提及,因此该算法的知名度与被掌握度都不如Dijkstra算法。事实上,有多种形式的Bellman-Ford算法的优化实现。这些优化实现在时间效率上得到相当提升,例如近一两年被热捧的SPFA(Shortest-Path Faster Algoithm 更快的最短路径算法)算法的时间效率甚至优于Dijkstra算法,因此成为信息学奥赛选手经常讨论的话题。然而,限于资料匮乏,有关Bellman-Ford算法的诸多问题常常困扰奥赛选手。如:该算法值得掌握么?怎样用编程语言具体实现?有哪些优化?与SPFA算法有关系么?本文试图对Bellman-Ford算法做一个比较全面的介绍。给出几种实现程序,从理论和实测两方面分析他们的时间复杂度,供大家在备战省选和后续的noi时参考。
Dijkstra算法无法处理负权边
dijkstra由于是贪心的,每次都找一个距源点最近的点(dmin),然后将该距离定为这个点到源点的最短路径(d[i]<--dmin);但如果存在负权边,那就有可能先通过并不是距源点最近的一个次优点(dmin'),再通过这个负权边L(L<0),使得路径之和更小(dmin'+L<dmin),则dmin'+L成为最短路径,并不是dmin,这样dijkstra就被囧掉了。
比如n=3,邻接矩阵:
0,3,4
3,0,-2
4,-2,0
用dijkstra求得d[1,2]=3,事实上d[1,2]=2,就是通过了1-3-2使得路径减小。
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] ←;                             //1阶段结束
        for i= to |v|- 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 遍松弛操作仍然会成功,这时,负权回路上的顶点不会收敛。
三、基本算法之上的优化
分析 Bellman-Ford算法,不难看出,外层循环(迭代次数)|v|-1实际上取得是上限。由上面对算法正确性的证明可知,需要的迭代遍数等于最短路径树的高度。如果不存在负权回路,平均情况下的最短路径树的高度应该远远小于 |v|-1,在此情况下,多余最短路径树高的迭代遍数就是时间上的浪费,由此,可以依次来实施优化。
从细节上分析,如果在某一遍迭代中,算法描述中第7行的松弛操作未执行,说明该遍迭代所有的边都没有被松弛。可以证明(怎么证明?):至此后,边集中所有的边都不需要再被松弛,从而可以提前结束迭代过程。这样,优化的措施就非常简单了。
设定一个布尔型标志变量 relaxed,初值为false。在内层循环中,仅当有边被成功松弛时,将 relaxed 设置为true。如果没有边被松弛,则提前结束外层循环。这一改进可以极大的减少外层循环的迭代次数。优化后的bellman-ford函数如下。
function bellmanford(s:longint):boolean;
     begin
        for i:= to nv do
          d[i]:=max;
        d[s]:=;
        for i:= to nv- do
         begin
           relaxed:=false;
            for j:= 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 ;
            relaxed:=true;
                 end;
           if not relaxed then break;
     end;
        for i:= to ne do
          if d[edges[j].e]>d[edges[j].s]+edges[j].w then exit(false);
        exit(true);
     end;
这样看似平凡的优化,会有怎样的效果呢?有研究表明,对于随机生成数据的平均情况,时间复杂度的估算公式为
1.13|E| if |E|<|V|
0.95*|E|*lg|V| if |E|>|V|
优化后的算法在处理有负权回路的测试数据时,由于每次都会有边被松弛,所以relaxed每次都会被置为true,因而不可能提前终止外层循环。这对应了最坏情况,其时间复杂度仍旧为O(VE)。
优化后的算法的时间复杂度已经和用二叉堆优化的Dijkstra算法相近了,而编码的复杂程度远比后者低。加之Bellman-Ford算法能处理各种边值权情况下的最短路径问题,因而还是非常优秀的。Usaco3.2.6 的程序见bellmanford_1.pas
四、SPFA 算法
SPFA是目前相当优秀的求最短路径的算法,值得我们掌握。
SPFA对Bellman-Ford算法优化的关键之处在于意识到:只有那些在前一遍松弛中改变了距离估计值的点,才可能引起他们的邻接点的距离估计值的改变。因此,用一个先进先出的队列来存放被成功松弛的顶点。初始时,源点s入队。当队列不为空时,取出对首顶点,对它的邻接点进行松弛。如果某个邻接点松弛成功,且该邻接点不在队列中,则将其入队。经过有限次的松弛操作后,队列将为空,算法结束。SPFA算法的实现,需要用到一个先进先出的队列 queue 和一个指示顶点是否在队列中的 标记数组 mark。为了方便查找某个顶点的邻接点,图采用临界表存储。
程序存储在 spfa.pas中。以usaco 3.2.6 试题2为例。用邻接表写的程序。
需要注意的是:仅当图不存在负权回路时,SPFA能正常工作。如果图存在负权回路,由于负权回路上的顶点无法收敛,总有顶点在入队和出队往返,队列无法为空,这种情况下SPFA无法正常结束。
判断负权回路的方案很多,世间流传最广的是记录每个结点进队次数,超过|V|次表示有负权
还有一种方法为记录这个结点在路径中处于的位置,ord[i],每次更新的时候ord[i]=ord[x]+1,若超过|V|则表示有负圈.....
其他方法还有很多,我反倒觉得流传最广的方法是最慢的.......
关于SPFA的时间复杂度,不好准确估计,一般认为是 O(kE),k是常数
SPFA实现
void Spfa()
{
for (int i = 0; i<num_town; ++i)//初始化
{
dis[i] = MAX;
visited[i] = false;
}
queue<int> Q;
dis[start] = ;
visited[start] = true;
Q.push(start);
while (!Q.empty()){
int temp = Q.front();
Q.pop();
for (int i=0; i<num_town; ++i)
{
if (dis[temp] + road[temp][i] < dis[i])//存在负权的话,就需要创建一个COUNT数组,当某点的入队次数超过V(顶点数)返回。
{
dis[i] = dis[temp] + road[temp][i];
if (!visited[i])
{
Q.push(i);
visited[i] = true;
}
}
}
visited[temp] = false;
}
}
五、时间效率实测
上述介绍的Bellman-Ford算法及两种的优化,只是在理论上分析了时间复杂度,用实际的数据测试,会有什么结果呢?为此,我们选择 usaco 3.2.6。
Spfa的时间效率还是很高的。并且spfa的编程复杂度要比Dijksta+heap优化要好的多。
Bellman-Ford & SPFA 算法——求解单源点最短路径问题的更多相关文章
- Dijkstra算法构造单源点最短路径
		
迪杰斯特拉(Dijkstra)算法 是求从某个源点到其余各顶点的最短路径,即对已知图 G=(V,E),给定源顶点 s∈V,找出 s 到图中其它各顶点的最短路径. 我总结下核心算法,伪代码如下: Dij ...
 - 【算法】单源最短路径和任意两点最短路径总结(补增:SPFA)
		
[Bellman-Ford算法] [算法]Bellman-Ford算法(单源最短路径问题)(判断负圈) 结构: #define MAX_V 10000 #define MAX_E 50000 int ...
 - 单源点最短路径的Dijkstra算法
		
在带权图(网)里,点A到点B所有路径中边的权值之和为最短的那一条路径,称为A,B两点之间的最短路径;并称路径上的第一个顶点为源点(Source),最后一个顶点为终点(Destination).在无权图 ...
 - Dijkstra单源点最短路径算法
		
学习参考: Dijkstra算法(单源最短路径) 最短路径—Dijkstra算法和Floyd算法 使用的图结构: 邻接矩阵: -1 20 -1 25 80-1 -1 40 -1 -1-1 -1 -1 ...
 - Dijkstra求解单源最短路径
		
Dijkstra(迪杰斯特拉)单源最短路径算法 Dijkstra思想 Dijkstra是一种求单源最短路径的算法. Dijkstra仅仅适用于非负权图,但是时间复杂度十分优秀. Dijkstra算法主 ...
 - GraphX中Pregel单源点最短路径(转)
		
原文链接:GraphX中Pregel单源点最短路径 GraphX中的单源点最短路径例子,使用的是类Pregel的方式. 核心部分是三个函数: 1.节点处理消息的函数 vprog: (VertexId ...
 - 51nod 1445 变色DNA  ( Bellman-Ford算法求单源最短路径)
		
1445 变色DNA 基准时间限制:1 秒 空间限制:131072 KB 分值: 40 难度:4级算法题 有一只特别的狼,它在每个夜晚会进行变色,研究发现它可以变成N种颜色之一,将这些颜色标号为0,1 ...
 - [数据结构与算法-15]单源最短路径(Dijkstra+SPFA)
		
单源最短路径 问题描述 分别求出从起点到其他所有点的最短路径,这次主要介绍两种算法,Dijkstra和SPFA.若无负权优先Dijkstra算法,存在负权选择SPFA算法. Dijkstra算法 非负 ...
 - Til the Cows Come Home(poj 2387 Dijkstra算法(单源最短路径))
		
Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 32824 Accepted: 11098 Description Bes ...
 
随机推荐
- safari穿越到chrome
			
tell application "Safari" set theURL to URL of front document set the clipboard to theURL ...
 - jquery获取第几个元素的方法总结
			
使用jquery时经常会遇到,选择器选择一组元素后,需要在这组元素中找到第几个元素. jquery中使用eq()方法找到第几个元素或第N个元素,jquery中eq()的使用如下: eq() 选择器选取 ...
 - JavaScript 遗漏知识再整理;错误处理,类型转换以及获取当前时间、年份、月份、日期;
			
1.JavaScript 错误处理 Throw.Try 和 Catch try 语句测试代码块的错误. catch 语句处理错误. throw 语句创建自定义错误. JavaScript 错误 当 J ...
 - C#部分---arraylist集合、arraylist集合中的object数据转换成int类string类等;间隔时间的表示方法;
			
ArrayList和Array的区别: 相同点:1.两者都实现了IList.ICollection.IEnumerable接口: 2.两者都可以使用证书索引访问集合中的元素,包括读取和赋值 ...
 - leetcode 114  Flatten Binary Tree to Linked List   ----- java
			
Given a binary tree, flatten it to a linked list in-place. For example,Given 1 / \ 2 5 / \ \ 3 4 6 T ...
 - Android——GridView(显示文字)
			
activity_test9的layout文件: <?xml version="1.0" encoding="utf-8"?> <Linear ...
 - PHP安全配置
			
vi /etc/php.ini #编辑 date.timezone = PRC #在946行把前面的分号去掉,改为date.timezone = PRC disable_functions = pas ...
 - java多线程之:SynchronousQueue队列
			
SynchronousQueue是这样一种阻塞队列,其中每个 put 必须等待一个 take,反之亦然.同步队列没有任何内部容量,甚至连一个队列的容量都没有. 不能在同步队列上进行 peek ...
 - What are the main disadvantages of Java Server Faces 2.0?
			
http://stackoverflow.com/questions/3623911/what-are-the-main-disadvantages-of-java-server-faces-2-0/ ...
 - Fixing Poor MySQL Default Configuration Values
			
I've recently been accumulating some MySQL configuration variables that have defaults which have pro ...