引入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;
}