单元最短路径--SPFA算法

SPFA算法

  (2012-09-16 19:12:37)
标签: 

杂谈

分类: 图论

介绍:  

    单源最短路径的算法最常用的是Dijkstra,些算法从时间复杂度来说为O(n^2),但是面对含有负权值的图来说就无能为力了,此时Dellman-ford算法就有用了,这咱算法是采用的是动态规化的思想,但是1994年西南交通大学段凡丁发表了SPFA(Shortest Path Faster Algorithm)听这个名字就懂了,这种算法在时间上一定很快了。它是对Dellman-ford的优化,所以建议今后直接学SPFA。很多时候,给定的图存在负权边,这时类似Dijkstra等算法便没有了用武之地,而Bellman-Ford算法的复杂度又过高,SPFA算法便派上用场了。

 

补:然而实际上算法时间复杂度很不稳定,有时候K可以达到指数级的,在稀疏图上表现相对更好(因为是靠边去推进的,而Dijkstra是靠点)


思路:
   简洁起见,我们约定有向加权图G不存在负权回路,即最短路径一定存在。当然,我们可以在执行该算法前做一次拓扑排序,以判断是否存在负权回路,但这不是我们讨论的重点。
和上文一样,我们用数组d记录每个结点的最短路径估计值,而且用邻接表来存储图G。我们采取的方法是动态逼近法:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且 v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。
  定理3 只要最短路径存在,上述SPFA算法必定能求出最小值。
证明:每次将点放入队尾,都是经过松弛操作达到的。换言之,每次的优化将会有某个点v的最短路径估计值d[v]变小。所以算法的执行会使d越来越小。由于我们假定图中不存在负权回路,所以每个结点都有最短路径值。因此,算法不会无限执行下去,随着d值的逐渐变小,直到到达最短路径值时,算法结束,这时的最短路径估计值就是对应结点的最短路径值。(证毕)
刚才我们只是笼统地说SPFA算法在效率上有过人之处,那么到底它的复杂度是怎样的?
  定理4 在平均情况下,SPFA算法的期望时间复杂度为O(E)。
证明:上述算法每次取出队首结点u,并访问u的所有临结点的复杂度为O(d),其中d为点u的出度。运用均摊分析的思想,对于|V|个点|E|条边的图,点的平均出度为,所以每处理一个点的复杂度为O( )。假设结点入队的次数h,显然h随图的不同而不同。但它仅与边的权值分布有关。我们设 h=kV,则算法SPFA的时间复杂度为。在平均的情况下,可以将k看成一个比较小的常数,所以SPFA算法在一般情况下的时间复杂度为O(E)。(证毕)
聪明的读者一定发现了,SPFA和经过简单优化的Bellman-Ford无论在思想上还是在复杂度上都有相似之处。确实如此。两者的思想都属于标号修正的范畴。算法是迭代式的,最短路径的估计值都是临时的。算法思想是不断地逼近最优解,只在最后一步才确定想要的结果。但是他们实现的方式上存在差异。正因为如此,它们的时间复杂度其实有较大差异的。在Bellman-Ford算法中,要是某个点的最短路径估计值更新了,那么我们必须对所有边指向的终点再做一次松弛操作;在SPFA算法中,某个点的最短路径估计值更新,只有以该点为起点的边指向的终点需要再做一次松弛操作。在极端情况下,后者的效率将是前者的n倍,一般情况下,后者的效率也比前者高出不少。基于两者在思想上的相似,可以这样说,SPFA算法其实是Bellman-Ford算法的一个进一步优化的版本。

 

算法流程

算法大致流程是用一个队列来进行维护。初始时将源加入队列。每次从队列中取出一个元素,并对所有与他相邻的点进行松弛,若某个相邻的点松弛成功,则将其入队。直到队列为空时算法结束。

这个算法,简单的说就是队列优化的bellman-ford,利用了每个点不会更新次数太多的特点发明的此算法

SPFA——Shortest Path Faster Algorithm,它可以在O(kE)的时间复杂度内求出源点到其他所有点的最短路径,可以处理负边。SPFA的实现甚至比Dijkstra或者Bellman_Ford还要简单:

设Dist代表S到I点的当前最短距离,Fa代表S到I的当前最短路径中I点之前的一个点的编号。开始时Dist全部为+∞,只有Dist[S]=0,Fa全部为0。

维护一个队列,里面存放所有需要进行迭代的点。初始时队列中只有一个点S。用一个布尔数组记录每个点是否处在队列中。

每次迭代,取出队头的点v,依次枚举从v出发的边v->u,设边的长度为len,判断Dist[v]+len是否小于 Dist[u],若小于则改进Dist[u],将Fa[u]记为v,并且由于S到u的最短距离变小了,有可能u可以改进其它的点,所以若u不在队列中,就将它放入队尾。这样一直迭代下去直到队列变空,也就是S到所有的最短距离都确定下来,结束算法。若一个点入队次数超过n,则有负权环

SPFA 在形式上和宽度优先搜索非常类似,不同的是宽度优先搜索中一个点出了队列就不可能重新进入队列,但是SPFA中一个点可能在出队列之后再次被放入队列,也就是一个点改进过其它的点之后,过了一段时间可能本身被改进,于是再次用来改进其它的点,这样反复迭代下去。设一个点用来作为迭代点对其它点进行改进的平均次数为k,有办法证明对于通常的情况,k在2左右。





   算法训练 最短路  
时间限制:1.0s   内存限制:256.0MB
   
问题描述

给定一个n个顶点,m条边的有向图(其中某些边权可能为负,但保证没有负环)。请你计算从1号点到其他点的最短路(顶点从1到n编号)。


输入格式


第一行两个整数n, m。
接下来的m行,每行有三个整数u, v, l,表示u到v有一条长度为l的边。


输出格式

共n-1行,第i行表示1号点到i+1号点的最短路。


样例输入

3 3
1 2 -1
2 3 -1
3 1 2


样例输出

-1
-2


数据规模与约定


对于10%的数据,n = 2,m = 2。

对于30%的数据,n <= 5,m <= 10。

对于100%的数据,1 <= n <= 20000,1 <= m <= 200000,-10000 <= l <= 10000,保证从任意顶点都能到达其他所有顶点。


题目链接:http://lx.lanqiao.org/problem.page?gpid=T15


题目分析:再记一下SPFA的vector版的板子


[cpp]  view plain  copy
  1. #include <cstdio>  
  2. #include <cstring>  
  3. #include <queue>  
  4. #include <vector>  
  5. using namespace std;  
  6. int const MAX = 200005;  
  7. int const INF = 1 << 30;  
  8. int n, m;  
  9.   
  10. struct EDGE  
  11. {  
  12.     int u, v;  
  13.     int val;  
  14. }e[MAX << 2];  
  15.   
  16. struct NODE  
  17. {  
  18.     int v, w;  
  19.     NODE(int vv, int ww)  
  20.     {  
  21.         v = vv;  
  22.         w = ww;  
  23.     }  
  24. };  
  25.   
  26. vector <NODE> vt[MAX];  
  27. int dis[MAX];  
  28. bool vis[MAX];  
  29.   
  30. void SPFA(int v0)  
  31. {  
  32.     memset(vis, falsesizeof(vis));  
  33.     for(int i = 1; i <= n; i++)  
  34.         dis[i] = INF;  
  35.     dis[v0] = 0;  
  36.     queue <int> q;  
  37.     q.push(v0);  
  38.     while(!q.empty())  
  39.     {  
  40.         int u = q.front();  
  41.         q.pop();  
  42.         vis[u] = false;           // 注意!!这里很重要
  43.         int sz = vt[u].size();  
  44.         for(int i = 0; i < sz; i++)  
  45.         {  
  46.             int v = vt[u][i].v;  
  47.             int w = vt[u][i].w;  
  48.             if(dis[v] > dis[u] + w)  
  49.             {  
  50.                 dis[v] = dis[u] + w;  
  51.                 if(!vis[v])  
  52.                 {  
  53.                     q.push(v);  
  54.                     vis[v] = true;  
  55.                 }  
  56.             }  
  57.         }  
  58.     }  
  59. }  
  60.   
  61. int main()  
  62. {  
  63.     scanf("%d %d", &n, &m);  
  64.     for(int i = 0; i < m; i++)  
  65.         scanf("%d %d %d", &e[i].u, &e[i].v, &e[i].val);  
  66.     for(int i = 0; i < m; i++)  
  67.         vt[e[i].u].push_back(NODE(e[i].v, e[i].val));  
  68.     SPFA(1);  
  69.     for(int i = 2; i <= n; i++)  
  70.         printf("%d\n", dis[i]);  
  71. }  

   算法训练 最短路  
时间限制:1.0s   内存限制:256.0MB
   
问题描述

给定一个n个顶点,m条边的有向图(其中某些边权可能为负,但保证没有负环)。请你计算从1号点到其他点的最短路(顶点从1到n编号)。


输入格式


第一行两个整数n, m。
接下来的m行,每行有三个整数u, v, l,表示u到v有一条长度为l的边。


输出格式

共n-1行,第i行表示1号点到i+1号点的最短路。


样例输入

3 3
1 2 -1
2 3 -1
3 1 2


样例输出

-1
-2


数据规模与约定


对于10%的数据,n = 2,m = 2。

对于30%的数据,n <= 5,m <= 10。

对于100%的数据,1 <= n <= 20000,1 <= m <= 200000,-10000 <= l <= 10000,保证从任意顶点都能到达其他所有顶点。


题目链接:http://lx.lanqiao.org/problem.page?gpid=T15


题目分析:再记一下SPFA的vector版的板子


[cpp]  view plain  copy
  1. #include <cstdio>  
  2. #include <cstring>  
  3. #include <queue>  
  4. #include <vector>  
  5. using namespace std;  
  6. int const MAX = 200005;  
  7. int const INF = 1 << 30;  
  8. int n, m;  
  9.   
  10. struct EDGE  
  11. {  
  12.     int u, v;  
  13.     int val;  
  14. }e[MAX << 2];  
  15.   
  16. struct NODE  
  17. {  
  18.     int v, w;  
  19.     NODE(int vv, int ww)  
  20.     {  
  21.         v = vv;  
  22.         w = ww;  
  23.     }  
  24. };  
  25.   
  26. vector <NODE> vt[MAX];  
  27. int dis[MAX];  
  28. bool vis[MAX];  
  29.   
  30. void SPFA(int v0)  
  31. {  
  32.     memset(vis, falsesizeof(vis));  
  33.     for(int i = 1; i <= n; i++)  
  34.         dis[i] = INF;  
  35.     dis[v0] = 0;  
  36.     queue <int> q;  
  37.     q.push(v0);  
  38.     while(!q.empty())  
  39.     {  
  40.         int u = q.front();  
  41.         q.pop();  
  42.         vis[u] = false;  
  43.         int sz = vt[u].size();  
  44.         for(int i = 0; i < sz; i++)  
  45.         {  
  46.             int v = vt[u][i].v;  
  47.             int w = vt[u][i].w;  
  48.             if(dis[v] > dis[u] + w)  
  49.             {  
  50.                 dis[v] = dis[u] + w;  
  51.                 if(!vis[v])  
  52.                 {  
  53.                     q.push(v);  
  54.                     vis[v] = true;  
  55.                 }  
  56.             }  
  57.         }  
  58.     }  
  59. }  
  60.   
  61. int main()  
  62. {  
  63.     scanf("%d %d", &n, &m);  
  64.     for(int i = 0; i < m; i++)  
  65.         scanf("%d %d %d", &e[i].u, &e[i].v, &e[i].val);  
  66.     for(int i = 0; i < m; i++)  
  67.         vt[e[i].u].push_back(NODE(e[i].v, e[i].val));  
  68.     SPFA(1);  
  69.     for(int i = 2; i <= n; i++)  
  70.         printf("%d\n", dis[i]);  
  71. }  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值