SPFA
介绍
SPFA(Shortest Path Faster Algorithm)(队列优化)算法是求单源最短路径的一种算法,在Bellman-ford算法的基础上加上一个队列优化,减少了冗余的松弛操作,是一种高效的最短路算法。
详见度娘SPFA
原理
请原谅我从度娘上无耻的转载,嘿嘿。
赶脚好冗杂。。。。
求单源最短路的SPFA算法的全称是:Shortest Path Faster Algorithm,是西南交通大学段凡丁于1994年发表的。从名字我们就可以看出,这种算法在效率上一定有过人之处。很多时候,给定的图存在负权边,这时类似Dijkstra等算法便没有了用武之地,而Bellman-Ford算法的复杂度又过高,SPFA算法便派上用场了。简洁起见,我们约定加权有向图G不存在负权回路,即最短路径一定存在。如果某个点进入队列的次数超过N次则存在负环(SPFA无法处理带负环的图)。当然,我们可以在执行该算法前做一次拓扑排序,以判断是否存在负权回路,但这不是我们讨论的重点。我们用数组d记录每个结点的最短路径估计值,而且用邻接表来存储图G。我们采取的方法是动态逼近法:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。定理: 只要最短路径存在,上述SPFA算法必定能求出最小值。
证明:每次将点放入队尾,都是经过松弛操作达到的。换言之,每次的优化将会有某个点v的最短路径估计值d[v]变小。所以算法的执行会使d越来越小。由于我们假定图中不存在负权回路,所以每个结点都有最短路径值。因此,算法不会无限执行下去,随着d值的逐渐变小,直到到达最短路径值时,算法结束,这时的最短路径估计值就是对应结点的最短路径值。
期望的时间复杂度:O(ke), 其中k为所有顶点进队的平均次数,可以证明k一般小于等于2。
对SPFA的一个很直观的理解就是由无权图的BFS转化而来。在无权图中,BFS首先到达的顶点所经历的路径一定是最短路(也就是经过的最少顶点数),所以此时利用数组记录节点访问可以使每个顶点只进队一次,但在带权图中,最先到达的顶点所计算出来的路径不一定是最短路。一个解决方法是放弃数组,此时所需时间自然就是指数级的,所以我们不能放弃数组,而是在处理一个已经在队列中且当前所得的路径比原来更好的顶点时,直接更新最优解。
(“算法编程后实际运算情况表明m一般没有超过2n.事实上顶点入队次数m是一个不容易事先分析出来的数,但它确是一个随图的不同而略有不同的常数.所谓常数,就是与e无关,与n也无关,仅与边的权值分布有关.一旦图确定,权值确定,原点确定,m就是一个确定的常数.所以SPFA算法复杂度为O(e)(证毕)”[1] ——SPFA的论文
但事实上这个证明是非常不严谨甚至错误的,事实上在bellman算法的论文中已有这方面的内容,所以国际上一般不承认SPFA算法)
SPFA算法有两个优化算法 SLF 和 LLL:
SLF:Small Label First 策略,设要加入的节点是j,队首元素为i, 若dist(j)〈 dist(i),则将j插入队首,否则插入队尾。
LLL:Large Label Last 策略,设队首元素为i,队列中所有dist值的平均值为x,若dist(i)>x则将i插入到队尾,查找下一元素,直到找到某一i使得dist(i)<=x,则将i出队进行松弛操作。 SLF 可使速度提高 15 ~ 20%;SLF + LLL 可提高约 50%。 在实际的应用中SPFA的算法时间效率不是很稳定,为了避免最坏情况的出现,通常使用效率更加稳定的Dijkstra算法。
个人理解
对于spfa可以依照着dijkstra来理解,首先从源点开始寻找与其直接相连的点以到源点的权值(显然是0)和源点到这些点的权值来更新到这些点的路经长,若某个点被更新,则将这个点加入到队列中,等待着下一次处理,若未被更新则显然对其后的点权值无任何影响,那么便不用在以该点为源点更新,因为没有任何改变,更新也保持不变。同时避免重复入列,对其进行标记限制,当一个点作为源点更新完与之相连的所有点之后,要将该点释放,标记去除,因为这个点在未来还有可能以别的点为源点更新,此时其要入队,所以要将不能入队的标记去掉。这样持续进行,直到以所有点为源点将每个点都更新一遍后,程序结束,求出以k为源点的到其他所有点的最短路。
代码如下
该程序未加入是否存在负权环的判断,加入该判断,只记录每个点入队次数,若入队超过n次则表明存在负权环。
program spfa;
type point=^rec;
rec=record
e,v:longint;
s:point;
end;
var queue:array[1..10000] of longint;
head,rear:longint;
vertex:array[1..1000] of point;
can:array[1..1000] of boolean;
dist:array[1..1000] of longint;
n,m,i,j,k,s,e,v:longint;
p:point;
procedure insert(s,e,v:longint);
begin
new(p);
p^.e:=e;
p^.v:=v;
p^.s:=vertex[s];
vertex[s]:=p;
end;
begin
assign(input,'D:/OI/input/spfa.txt');
reset(input);
assign(output,'D:/OI/output/spfa.txt');
rewrite(output);
fillchar(can,sizeof(can),true);
readln(n,m,k);
for i:=1 to n do
begin
dist[i]:=maxlongint;
vertex[i]:=nil;
end;
for i:=1 to m do
begin
readln(s,e,v);
insert(s,e,v);
end;
dist[k]:=0;
head:=1;
rear:=1;
queue[head]:=k;
can[k]:=false;
while head<=rear do {spfa 更新每个点得到单源最短路}
begin
p:=vertex[queue[head]];
while p<>nil do
begin
if dist[p^.e]>dist[queue[head]]+p^.v {松弛操作,其实就是选短的路。。。呃呃}
then
begin
dist[p^.e]:=dist[queue[head]]+p^.v;
if can[p^.e] {如果未在队列中且又被更新则入队,已在队列中显然无需重复入队}
then
begin
inc(rear);
queue[rear]:=p^.e;
can[p^.e]:=false;
end;
end;
p:=p^.s;
end;
can[queue[head]]:=true;
inc(head);
end;
for i:=1 to n do write(dist[i],' ');
close(input);
close(output);
end.
每日一句
当我们一无所有的时候,我们也能够说:我很幸福。因为我们还有健康的身体。当我们不再享有健康的时候,那些最勇敢的人可以依然微笑着说:我很幸福。因为我还有一颗健康的心。甚至当我们连心也不再存在的时候,那些人类最优秀的分子仍旧可以对宇宙大声说:我很幸福。因为我曾经生活过。常常提醒自己注意幸福,就像在寒冷的日子里经常看看太阳,心就不知不觉暖洋洋亮光光。