先给出题面:https://vjudge.net/problem/UVA-558
题意描述:给你含n个点以及m条边的图,让你判断在这个图中是否存在负权回路。
首先,我们来介绍什么是SPEA算法
SPFA算法是求解单源最短路径问题的一种算法,由理查德·贝尔曼(Richard Bellman) 和 莱斯特·福特 创立的。有时候这种算法也被称为 Moore-Bellman-Ford 算法,因为 Edward F. Moore 也为这个算法的发展做出了贡献。它的原理是对图进行V-1次松弛操作,得到所有可能的最短路径。其优于迪科斯彻算法的方面是边的权值可以为负数、实现简单,缺点是时间复杂度过高,高达 O(VE)。但算法可以进行若干种优化,提高了效率。
这个算法的思路是:
用数组dis记录每个结点的最短路径估计值,用邻接表或邻接矩阵来存储图G。我们采取的方法是动态逼近法:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。
具体来说,其核心代码实现思路是:
for(int k = 1;k<=n-1;k++) for(int i = 1;i<=m;i++) if(dis[v[i] > dis[u[i] + w[i]) dis[v[i]] = dis[u[i]] + w[i];
看到这段代码是不是有些熟悉,觉得有点像Bellmen-Ford算法,事实上,SPEA就是Bellmen-Ford算法的队列优化。
因此,要想理解SPEA算法,就应该先了解Bellmen-Ford算法,有时间我也会补上一篇关于Bellmen-Ford算法的博客的。
在这里,我就简要的说一下Bellmen-Ford算法的思路
用一个dis数组记录图中各个点到源点(下面将以0为源点讲解)的距离,将dis[0] 初始化为0,其他各点初始化为INF(即无穷大,一般根据题目条件来定),接下来进行n-1轮松弛操作,参考上面的代码,就是判断对于某个点v[i],能否通过源点 0 到 点u[i] ,再从点u[i] 到 点v[i] 这条路径来缩小 源点 0 到 v[i] 的距离。进行完第一轮循环之后,我们得到dis数组事实上就是从0号点出发,只经过一条边,到各个点的最短路径,因为最短路径事实上最多有n-1条边,所以这种松弛会进行n-1操作。最终,我们得到就是从源点0出发,经过给出的m条边,到各个点的最短路。此时的算法复杂度为O(n*m)。
接下来,我们思考来如何优化一下
我们真的需要进行n-1松弛吗?事实上,如果在某一轮松弛中,dis数组没有发生任何更新,那么在这次松弛之后所有的松弛也一定不会改变dis数组,所以后面所有的循环都是没有必要的,因此,我们可以加入一个flag,来判断这一轮松弛是否更新了dis数组,如果没有更新,直接跳出即可。具体代码实现如下:
1 for(int k = 1;k<=n-1;k++) 2 { 3 bool flag = true; 4 for(int i = 1;i<=m;i++) 5 if(dis[v[i] > dis[u[i] + w[i]) 6 { 7 dis[v[i]] = dis[u[i]] + w[i]; 8 flag = false; 9 } 10 if(flag) 11 break; 12 }
接下来,我们继续来想,什么时候dis[v[i]]的值可能会发生改变,从上面给出的式子来说,因为w[i]是根据输入定的,所以不可改变,所以,只有当dis[u[i]]的值发生改变时,dis[v[i]]的值才可能会发生改变,那么也就是说,只有当与dis值改变的点的点的dis值才有可能改变,有点绕,可能需要思考一段时间。所以我们可以把每次松弛操作中dis值改变的点放在集合中,下次只对与这些点相连的点进行松弛,必将这些点放进集合中,但是重复的点不应该出现在集合中,所以我们要有一个vis数组判断当前这个点是否在集合中。代码实现如下(此处用邻接表存的边,vec[u][i].to表示有一条从u指向它的边,权值为vec[u][i].val):
1 queue<int>q; 2 q.push(0); 3 vis[0] = 1; 4 while(!q.empty()) 5 { 6 int d = q.front(); 7 q.pop(); 8 vis[d] = 0; 9 for(int i = 0;i<vec[d].size();i++) 10 { 11 if(dis[vec[d][i].to]>dis[d]+vec[d][i].val) 12 { 13 dis[vec[d][i].to]=dis[d]+vec[d][i].val; 14 if(!vis[vec[d][i].to]) 15 { 16 vis[vec[d][i].to] = 1; 17 q.push(vec[d][i].to); 18 } 19 20 } 21 } 22 }
这也就是SPEA算法的真正核心代码。
接下来我们来思考如何判断是否存在负权回路,假设一个图中存在一个负权回路,那这个图中是不存在最短路的,因为每走一次负权回路,源点到某些点的距离一定会改变的。对应上面代码,就会出现死循环,因为有些点的dis值会一直改变,也就是有些点会进入队列很多次,这是一个突破口,那么一个点到底进入队列多少次算是存在负权回路那,思考最开始说的,图的最短路最多有n-1条边,所以,一个点最多应该进入队列n-1次,因此,当一个进入队列的次数大于等于n次时,该图中就存在负权回路。因此我们只要在上面代码中加上这一条件的判断就能判断是否存在负权回路了,最后给出本题的ac代码。
1 #include <cstdio> 2 #include <queue> 3 #include <vector> 4 using namespace std; 5 const int maxn = 1000+5; 6 const int INF = 0x3f3f3f3f; 7 struct V 8 { 9 int to,val; 10 V(int a,int b):to(a),val(b){} 11 }; 12 vector<V>vec[maxn]; 13 int vis[maxn],dis[maxn],num[maxn]; 14 int n,m; 15 void init() 16 { 17 for(int i = 0;i<n;i++) 18 { 19 vec[i].clear(); 20 vis[i] = 0; 21 num[i] = 0; 22 dis[i] = INF; 23 } 24 dis[0] = 0; 25 } 26 bool check() 27 { 28 queue<int>q; 29 q.push(0); 30 vis[0] = 1; 31 num[0]++; 32 while(!q.empty()) 33 { 34 int d = q.front(); 35 36 q.pop(); 37 vis[d] = 0; 38 for(int i = 0;i<vec[d].size();i++) 39 { 40 41 //printf("d = %d,%d,%d ",d,dis[vec[d][i].to],dis[d]+vec[d][i].val); 42 if(dis[vec[d][i].to]>dis[d]+vec[d][i].val) 43 { 44 dis[vec[d][i].to]=dis[d]+vec[d][i].val; 45 if(!vis[vec[d][i].to]) 46 { 47 num[vec[d][i].to]++; 48 vis[vec[d][i].to] = 1; 49 q.push(vec[d][i].to); 50 51 if(num[vec[d][i].to]>=n) 52 { 53 return true; 54 } 55 } 56 57 } 58 } 59 } 60 return false; 61 } 62 int main() 63 { 64 //freopen("out.txt","w",stdout); 65 int T; 66 scanf("%d",&T); 67 while(T--) 68 { 69 scanf("%d %d",&n,&m); 70 init(); 71 int a,b,c; 72 for(int i = 0;i<m;i++) 73 { 74 scanf("%d %d %d",&a,&b,&c); 75 vec[a].push_back(V(b,c)); 76 //vec[b].push_back(V(a,c)); 77 } 78 if(check()) 79 printf("possible "); 80 else 81 printf("not possible "); 82 } 83 return 0; 84 }
本文结束....