spfa算法

SPFA算法

  编辑
SPFA(Shortest Path Faster Algorithm)(队列优化)算法是求单源 最短路径的一种算法,它还有一个重要的功能是判负环(在差分约束系统中会得以体现),在 Bellman-ford算法的基础上加上一个队列优化,减少了冗余的 松弛操作,是一种高效的最短路算法。
简    称
SPFA
全    称
Shortest Path Faster Algorithm
提出者
段凡丁

原理及证明

编辑
最短路问题 最短路问题
求单源最短路的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(me), 其中m为所有顶点进队的平均次数,可以证明m一般小于等于2n:“算法编程后实际运算情况表明m一般没有超过2n.事实上顶点入队次数m是一个不容易事先分析出来的数,但它确是一个随图的不同而略有不同的常数.所谓常数,就是与e无关,与n也无关,仅与边的权值分布有关.一旦图确定,权值确定,原点确定,m就是一个确定的常数.所以SPFA算法复杂度为O(e).证毕."(SPFA的论文)不过,这个证明是非常不严谨甚至错误的,事实上在 bellman算法的论文中已有这方面的内容,所以国际上一般不承认SPFA算法。
对SPFA的一个很直观的理解就是由无权图的 BFS转化而来。在无权图中,BFS首先到达的顶点所经历的路径一定是最短路(也就是经过的最少顶点数),所以此时利用数组记录节点访问可以使每个顶点只进队一次,但在带权图中,最先到达的顶点所计算出来的路径不一定是最短路。一个解决方法是放弃数组,此时所需时间自然就是指数级的,所以我们不能放弃数组,而是在处理一个已经在队列中且当前所得的路径比原来更好的顶点时,直接更新最优解。
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实际上是Bellman-Ford基础上的队列优化
一种伪代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Procedure SPFA;
  Begin 
  initialize-single-source(G,s); 
  initialize-queue(Q); 
  enqueue(Q,s); 
  while  not empty(Q)  do  
  begin     
  u:=dequeue(Q);     
  for  each v∈adj[u]  do 
  begin         
  tmp:=d[v];   
        relax(u,v);      
     if  (tmp<>d[v]) and (not v in Q) then     
        enqueue(Q,v);   
      end;   
   end;
End;
一种更容易读懂的伪代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ProcedureSPFA;
Begin
     initialize- single -source(G,s);
     initialize-queue(Q);
     enqueue(Q,s);
     while  not  empty(Q)  do  begin
         u:=dequeue(Q);
         for  each v∈adj[u]  do  begin
             tmp:=d[v];
             relax(u,v);
             if (tmp<>d[v]) and ( not  in  Q) then  enqueue(Q,v);
         end ;
     end ;
End

C++代码

编辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include<iostream>
#include<vector>
#include<list>
using  namespace  std;
struct  Edge
{
     int  to,len;
};
bool  spfa( const  int  &beg, //出发点
           const  vector<list<Edge> > &adjlist, //邻接表,通过传引用避免拷贝
           vector< int > &dist, //出发点到各点的最短路径长度
           vector< int > &path) //路径上到达该点的前一个点
//没有负权回路返回0
//福利:这个函数没有调用任何全局变量,可以直接复制!
{
     const  int  INF=0x7FFFFFFF,NODE=adjlist.size(); //用邻接表的大小传递顶点个数,减少参数传递
     dist.assign(NODE,INF); //初始化距离为无穷大
     path.assign(NODE,-1); //初始化路径为未知
     list< int > que(1,beg); //处理队列
     vector< int > cnt(NODE,0); //记录各点入队次数,用于判断负权回路
     vector< bool > flag(NODE,0); //标志数组,判断是否在队列中
     dist[beg]=0; //出发点到自身路径长度为0
     cnt[beg]=flag[beg]=1; //入队并开始计数
     while (!que.empty())
     {
         const  int  now=que.front();
         que.pop_front();
         flag[now]=0; //将当前处理的点出队
         for (list<Edge>::const_iterator //用常量迭代器遍历邻接表
                 i=adjlist[now].begin(); i!=adjlist[now].end(); ++i)
             if (dist[i->to]>dist[now]+i->len) //不满足三角不等式
             {
                 dist[i->to]=dist[now]+i->len; //更新
                 path[i->to]=now; //记录路径
                 if (!flag[i->to]) //若未在处理队列中
                 {
                     if (NODE==++cnt[i->to]) return  1; //计数后出现负权回路
                     if (!que.empty()&&dist[i->to]<dist[que.front()]) //队列非空且优于队首(SLF)
                         que.push_front(i->to); //放在队首
                     else  que.push_back(i->to); //否则放在队尾
                     flag[i->to]=1; //入队
                 }
             }
     }
     return  0;
}
int  main()
{
     int  n_num,e_num,beg; //含义见下
     cout<< "输入点数、边数、出发点:" ;
     cin>>n_num>>e_num>>beg;
     vector<list<Edge> > adjlist(n_num,list<Edge>()); //默认初始化邻接表
     for ( int  i=0,p; i!=e_num; ++i)
     {
         Edge tmp;
         cout<< "输入第" <<i+1<< "条边的起点、终点、长度:" ;
         cin>>p>>tmp.to>>tmp.len;
         adjlist[p].push_back(tmp);
     }
     vector< int > dist,path; //用于接收最短路径长度及路径各点
     if (spfa(beg,adjlist,dist,path))cout<< "图中存在负权回路\n" ;
     else  for ( int  i=0; i!=n_num; ++i)
         {
             cout<<beg<< "到" <<i<< "的最短距离为" <<dist[i]<< ",反向打印路径:" ;
             for ( int  w=i; path[w]>=0; w=path[w])cout<<w<< "<-" ;
             cout<<beg<< '\n' ;
         }
}
//这段c++代码在某些情况下会runtime error
//楼上是不是没注意点是从0开始的

pascal代码

编辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
const
   maxp= 10000 ; {最大结点数}
var {变量定义}
   p,c,s,t: longint ; {p,结点数;c,边数;s:起点;t:终点}
   a,b: array [ 1.. maxp, 0.. maxp] of  longint ; {a[x,y]存x,y之间边的权;b[x,c]存与x相连的第c个边的另一个结点y}
   d,m: array [ 1.. maxp] of  integer ; {d:队列,m:入队次数标记}
   v: array [ 1.. maxp] of  boolean ; {是否入队的标记}
   dist: array [ 1.. maxp] of  longint ; {到起点的最短路}
   head,tail: longint ; {队首/队尾指针}
procedure  init;
var
   i,x,y,z: longint ;
begin
   read(p,c);
   for  i:= 1  to  do  begin
     readln(x,y,z); {x,y:一条边的两个结点;z:这条边的权值}
     inc(b[x, 0 ]);b[x,b[x, 0 ]]:=y;a[x,y]:=z; {b[x,0]:以x为一个结点的边的条数}
     inc(b[y, 0 ]);b[y,b[y, 0 ]]:=x;a[y,x]:=z;
   end ;
   readln(s,t); {读入起点与终点}
end ;
 
procedure  spfa(s: longint ); {SPFA}
var
   i,j,now: longint ;
begin
   fillchar(d,sizeof(d), 0 );
   fillchar(v,sizeof(v), false );
   for  j:= 1  to  do  dist[j]:=maxlongint;
   dist[s]:= 0 ; v[s]:= true ; d[ 1 ]:=s;  {队列的初始状态,s为起点}
   head:= 1 ; tail:= 1 ;
   while  head<=tail  do {队列不空}
   begin
     now:=d[head]; {取队首元素}
     for  i:= 1  to  b[now, 0 do
       if  dist[b[now,i]]>dist[now]+a[now,b[now,i]]  then
       begin
         dist[b[now,i]]:=dist[now]+a[now,b[now,i]]; {修改最短路}
         if  not  v[b[now,i]]  then {扩展结点入队}
         begin
           inc(m[b[now,i]]);
           if  m[b[now,i]]=p  then  begin  writeln ( 'no way' );halt; end ;
                                                 {同一节点入队次数超过p,存在负环}
           inc(tail);
           d[tail]:=b[now,i];
           v[b[now,i]]:= true ;
         end ;
       end ;
     v[now]:= false ; {释放结点,一定要释放掉,因为这节点有可能下次用来松弛其它节点}
     inc(head); {出队}
   end ;
end ;
 
procedure  print;
begin
   writeln (dist[t]);
end ;
 
begin
   init;
   spfa(s);
   print;
end .

解决实际问题

编辑
poj: MPI Maelstrom
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include<cstdio>
#include<iostream>
#include<cstring>
using  namespace  std;
struct  node
{
     int  x,y,d,next;
};
int  num=0;
node v[11100];
int  first[105];
int  n;
int  q[105];
int  f[105];
bool  p[105];
int  start=1,end=2;
char  map[105][5055];
void  connect( int  x, int  y, int  d)
{
     num++;
     v[num].x=x;v[num].y=y;v[num].d=d;
     v[num].next=first[x];first[x]=num;
     return
}
int  main()
{
     int  temp;
     int  ans=0;
     memset (first,0, sizeof (first));
     memset (q,0, sizeof (q)); q[1]=1;
     memset (f,63, sizeof (f)); temp=f[1];f[1]=0;
     memset (p, false , sizeof (p)); p[1]= true ;
     scanf  ( "%d\n" ,&n);
     for  ( int  i=1;i<n;i++)
     {
         int  x=(i+1),y=1,d=0;
         gets (map[i]);
         for  ( int  j=0;j< strlen (map[i]);j++)
         {
             if  (map[i][j]!= ' '  && map[i][j]!= 'x' ) {d*=10;d+=(map[i][j]- '0' );}
             if  (map[i][j]== ' '  || j== strlen (map[i])-1)
             {
                 if  (map[i][j-1]!= 'x'  && map[i][j]!= 'x' )
                 {
                     connect(y,x,d);
                     connect(x,y,d);
                 }
                 d=0;y++;
             }
         }
     }
     while  (start!=end)
     {
         int  x=q[start];
         for  ( int  i=first[x];i!=0;i=v[i].next)
         {
             int  y=v[i].y;
             if  (f[y]>f[x]+v[i].d)
             {
                 f[y]=f[x]+v[i].d;
                 if  (p[y]== false )
                 {
                     q[end]=y;
                     p[y]= true ;
                     end++;
                     if  (end>n) end=1;
                 }
             }
         }
         p[x]= false ;
         start++;
         if  (start>n) start=1;
     }
     for  ( int  i=1;i<=n;i++)
     {
         if  (f[i]!=temp && f[i]>ans) ans=f[i];
     }
     printf  ( "%d" ,ans);
     return  0;
}
































粗略讲讲SPFA算法的原理,SPFA算法是1994年西安交通大学段凡丁提出

是一种求单源最短路的算法

算法中需要用到的主要变量

int n;  //表示n个点,从1到n标号

int s,t;  //s为源点,t为终点

int d[N];  //d[i]表示源点s到点i的最短路

int p[N];  //记录路径(或者说记录前驱)

queue <int> q;  //一个队列,用STL实现,当然可有手打队列,无所谓

bool vis[N];   //vis[i]=1表示点i在队列中 vis[i]=0表示不在队列中

 

几乎所有的最短路算法其步骤都可以分为两步

1.初始化

2.松弛操作

 

初始化: d数组全部赋值为INF(无穷大);p数组全部赋值为s(即源点),或者赋值为-1,表示还没有知道前驱

             然后d[s]=0;  表示源点不用求最短路径,或者说最短路就是0。将源点入队;

    (另外记住在整个算法中有顶点入队了要记得标记vis数组,有顶点出队了记得消除那个标记)

队列+松弛操作

读取队头顶点u,并将队头顶点u出队(记得消除标记);将与点u相连的所有点v进行松弛操作,如果能更新估计值(即令d[v]变小),那么就更新,另外,如果点v没有在队列中,那么要将点v入队(记得标记),如果已经在队列中了,那么就不用入队

以此循环,直到队空为止就完成了单源最短路的求解

 

SPFA可以处理负权边

定理: 只要最短路径存在,上述SPFA算法必定能求出最小值。

证明:

  每次将点放入队尾,都是经过松弛操作达到的。换言之,每次的优化将会有某个点v的最短路径估计值d[v]变小。所以算法的执行会使d越来越小。由于我们假定图中不存在负权回路,所以每个结点都有最短路径值。因此,算法不会无限执行下去,随着d值的逐渐变小,直到到达最短路径值时,算法结束,这时的最短路径估计值就是对应结点的最短路径值。(证毕)

期望的时间复杂度O(ke), 其中k为所有顶点进队的平均次数,可以证明k一般小于等于2。

 

判断有无负环:

  如果某个点进入队列的次数超过N次则存在负环(SPFA无法处理带负环的图)

 

 

 

SPFA的两种写法,bfs和dfs,bfs判别负环不稳定,相当于限深度搜索,但是设置得好的话还是没问题的,dfs的话判断负环很快

 

复制代码
int spfa_bfs(int s)
{
    queue <int> q;
    memset(d,0x3f,sizeof(d));
    d[s]=0;
    memset(c,0,sizeof(c));
    memset(vis,0,sizeof(vis));

    q.push(s);  vis[s]=1; c[s]=1;
    //顶点入队vis要做标记,另外要统计顶点的入队次数
    int OK=1;
    while(!q.empty())
    {
        int x;
        x=q.front(); q.pop();  vis[x]=0;
        //队头元素出队,并且消除标记
        for(int k=f[x]; k!=0; k=nnext[k]) //遍历顶点x的邻接表
        {
            int y=v[k];
            if( d[x]+w[k] < d[y])
            {
                d[y]=d[x]+w[k];  //松弛
                if(!vis[y])  //顶点y不在队内
                {
                    vis[y]=1;    //标记
                    c[y]++;      //统计次数
                    q.push(y);   //入队
                    if(c[y]>NN)  //超过入队次数上限,说明有负环
                        return OK=0;
                }
            }
        }
    }

    return OK;

}
复制代码

 

复制代码
int spfa_dfs(int u)
{
    vis[u]=1;
    for(int k=f[u]; k!=0; k=e[k].next)
    {
        int v=e[k].v,w=e[k].w;
        if( d[u]+w < d[v] )
        {
            d[v]=d[u]+w;
            if(!vis[v])
            {
                if(spfa_dfs(v))
                    return 1;
            }
            else
                return 1;
        }
    }
    vis[u]=0;
    return 0;
}


词条标签:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值