最短路径之spfa
引入1:单源最短路
问:求带权有向图上一个源点到其他点的最短路径距离
如果没有非负边权,我们自然可以想到dij。但是如果有负边权呢?这时候就要用SPFA算法求解。
原理&讲解
用dis数组记录源点到有向图上任意一点距离,其中源点到自身距离为0,到其他点距离为INF。将源点入队,并重复以下步骤:
- 队首x出队
- 遍历所有以队首为起点的有向边(x,i),若dis[x]+w(x,i)<dis[i],则更新dis[i]
- 如果点i不在队列中,则i入队
- 若队列为空,跳出循环;否则执行1
实际上我们可以将其理解为bfs
如果图是随机生成的,时间复杂度为 O(KM) (K可以认为是个常数,m为边数,n为点数)
但是实际上SPFA的算法复杂度是 O(NM) ,可以构造出卡SPFA的数据,让SPFA超时。
在NOI 2018的第一天第一题中,出题人卡了SPFA算法,导致100分变成60分,所以在没有负环、单纯求最短路径,不建议使用SPFA算法,而是用Dijkstra算法。
在初一我们学到一条三角形中的性质,即同一三角形内两边之和大于第三边。而最短路中如u->v的最短路它是小于等于其它任意路径的,这使我们容易yy到三角形。也就是说,我们实际上每次都是在判断这条路径符不符合三角形不等式,若不符合,我们就将原先的路径松弛为现在的路径,使得现在的路径满足三角形不等式。但是为什么松弛后要将终点入队呢?SPFA的过程是BFS,它是不停扩展节点的。而当我们更新了这一条路径,那么可能会出现基于这一条路径的新路,我们需要判断原路与新路是否满足三角形不等式。
模拟&代码
我们可以手推这张图模拟一下~
我们以1为源点,初始化:dis[源点]=0,其他为正无穷,并将源点入队。
队首1出队,并枚举它的出边1->2,1->3。由dis[1]+w(1,2)=1<dis[2]=INF,dis[1]+w(1,3)=6<dis[3]=INF得dis[2]=dis[1]+w(1,2)=1,dis[3]=dis[1]+w(1,3)=6,并将2,3入队。
队首2出队,枚举它的出边2->3,2->4,2->5。都不满足三角形不等式,所以松弛它们。并将3,4,5入队,但由于3已在队内,所以不管。
队首3出队,没有能松弛的边,直接略过。
此时队内剩下4,5,由于这两点没有出边,所以在此不枚举。
手绘勿喷
下面是带注释代码:
#include<iostream> #include<vector> #include<algorithm> #include<cstring> #include<string> #include<cstdio> #include<cstdlib> #include<queue> #define N 110000 #define INF 0x3f3f3f3f using namespace std; int n,m,a,b,c,vis[N],dis[N]; struct node { int d,w; };//定义一个结构体来存储每个入度点以及对应边的权值 //比如边u->v,权值为w,node结构体存储的就是v以及w。 vector<node>v[N]; void spfa(int u); int main() { //对于N非常大但是M很小的这种稀疏图来说,用邻接矩阵N*N是存不下的。邻接矩阵是将所有的点都存储下来了,然而 //对于稀疏图来说,有很多点是没有用到的,把这些点也存储下来的话就会很浪费空间。可以用邻接表来存储,这里借助vector来实现邻接表的操作。 //用邻接表存储时候,只存储有用的点,对于没有用的点不存储,实现空间的优化。 cin>>n>>m; for(int i=0; i<=n; i++) v[i].clear();//将vecort数组清空 for(int i=1; i<=m; i++) //用vector存储邻接表 { node nd; scanf("%d%d%d",&a,&b,&c); nd.d=b,nd.w=c;//将入度的点和权值赋值给结构体 v[a].push_back(nd);//将每一个从a出发能直接到达的点都压到下标为a的vector数组中,以后遍历从a能到达的点就可以直接遍历v[a] // nd.d=a,nd.w=c;//无向图的双向存边 // v[b].push_back(nd); } spfa(1); if(dis[n]!=INF) printf("%d\n",dis[n]); else printf("impossible"); return 0; } void spfa(int u){ memset(vis,1,sizeof(vis)); memset(dis,0x3f,sizeof(dis)); dis[u]=0; queue<int> q; q.push(u); vis[u]=false; while (!q.empty()) { int x=q.front(); q.pop(); vis[x]=true; vector<node> s=v[x]; for (int i = 0; i < s.size(); ++i) { int v=s[i].d; if(dis[x]+s[i].w<dis[v]){ dis[v]=dis[x]+s[i].w; if(vis[v]){ q.push(v); vis[v]=false; } } } } }
引入2:判正(负)环
spfa算法还可以在有向图内判正环负环,我们可以使用DFS/BFS版SPFA。注意,判负环跑最短路,判正环跑最长路。
#include<iostream> #include<vector> #include<algorithm> #include<cstring> #include<string> #include<cstdio> #include<cstdlib> #include<queue> #define N 110000 #define INF 0x3f3f3f3f using namespace std; int n,m,a,b,c,instack[N],dis[N],flag; struct node { int d,w; };//定义一个结构体来存储每个入度点以及对应边的权值 //比如边u->v,权值为w,node结构体存储的就是v以及w。 vector<node>v[N]; void spfa(int u); int main() { //对于N非常大但是M很小的这种稀疏图来说,用邻接矩阵N*N是存不下的。邻接矩阵是将所有的点都存储下来了,然而 //对于稀疏图来说,有很多点是没有用到的,把这些点也存储下来的话就会很浪费空间。可以用邻接表来存储,这里借助vector来实现邻接表的操作。 //用邻接表存储时候,只存储有用的点,对于没有用的点不存储,实现空间的优化。 cin>>n>>m; for(int i=0; i<=n; i++) v[i].clear();//将vecort数组清空 for(int i=1; i<=m; i++) //用vector存储邻接表 { node nd; scanf("%d%d%d",&a,&b,&c); nd.d=b,nd.w=c;//将入度的点和权值赋值给结构体 v[a].push_back(nd);//将每一个从a出发能直接到达的点都压到下标为a的vector数组中,以后遍历从a能到达的点就可以直接遍历v[a] // nd.d=a,nd.w=c;//无向图的双向存边 // v[b].push_back(nd); } memset(instack,0,sizeof(instack)); memset(dis,0,sizeof(dis)); flag=0; for(int i=1;i<=n;i++){spfa(i);if(flag)break;} if(flag)printf("Yes"); else printf("No"); return 0; } void spfa(int u){ if(instack[u]){ flag=1; return; } instack[u]=true; vector<node> s=v[u]; for (int i = 0; i < s.size(); ++i) { if(dis[u]+s[i].w<dis[s[i].d]){ dis[s[i].d]=dis[u]+s[i].w; spfa(s[i].d); if(flag)return; } } instack[u]=false; }