SPFA已经死了… 当然,只是在出题人针对的情况下,容易退化为Bellman-ford的复杂度,在处理负权图方面,dijkstra做不到,所以它仍有着dijkstra所不能及的优势(当然负权图也还能卡SPFA,使其达到指数复杂度 )。
情景:
给出一个有向图,请输出从某一点s出发到所有点的最短路径长度,若不可达,输出
2
31
−
1
2^{31}-1
231−1(即URA)。为了方便,代码中将结点编号 1~n 改为 0~n-1,最后再以 1~n 输出。‘
采用邻接表G形式存储图。
与dijkstra的堆优化相比,可以发现,dijkstra中堆是对进行松弛操作后的源点到各点的距离进行贪心,即构建<距离,结点>的结构体,而SPFA是将进行了松弛操作的后继加入队列,然后以队首后继作为新一轮搜索的前趋,搜索该点各个边,这样看采用队列的SPFA是一种bfs思想。
若需要输出最短路径,参考dijkstra模板,同样只需要设置前趋数组pre,在进行松弛操作后存储点的前趋,最后从终点往前回溯即可。
若需要练习可移步例题:洛谷-【模板】单源最短路径(弱化版)
队列的SPFA
同一时间内队列中不能存在同个结点,所以需要vis数组辅助
#include<iostream>
#include<queue>
#include<vector>
#include<stack>
using namespace std;
const int URA=(2<<30)-1;
const int MAX_N=1e4+10;
const int INF=1e9+10;
struct edge
{
int to,cost;
edge(int t,int c):to(t),cost(c){}
};
int n,m,s;
int dis[MAX_N];
vector<edge> G[MAX_N];
bool vis[MAX_N];
void SPFA(int s)
{
queue<int> q;
for(int i=0;i<n;i++)
dis[i]=INF;
dis[s]=0;
q.push(s);
while(!q.empty())
{
int u=q.front();
q.pop();
vis[u]=false;//出队,但可以重新进入
for(int i=0;i<G[u].size();i++)
{
int v=G[u][i].to;
if(dis[u]+G[u][i].cost<dis[v])
{
dis[v]=dis[u]+G[u][i].cost;
if(!vis[v])//不在队列中
{
vis[v]=true;
q.push(v);
}
}
}
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>m>>s;
int u,v,w;
for(int i=0;i<m;i++)
{
cin>>u>>v>>w;
G[u-1].push_back(edge(v-1,w));
}
SPFA(s-1);
for(int i=0;i<n;i++)
cout<<(dis[i]==INF?URA:dis[i])<<" ";
return 0;
}
堆的SPFA:
堆中允许存在多个同名结点,因此不需要vis数组。在一些非负权图中表现不错,但是在带负权边的图中容易被卡导致复杂度暴增。
#include<iostream>
#include<queue>
#include<vector>
#include<stack>
using namespace std;
const int URA=(2<<30)-1;
const int MAX_N=1e4+10;
const int INF=1e9+10;
struct edge
{
int to,cost;
edge(int t,int c):to(t),cost(c){}
};
int n,m,s;
int dis[MAX_N];
vector<edge> G[MAX_N];
void SPFA(int s)
{
priority_queue<int,vector<int>,greater<int> >q;
for(int i=0;i<n;i++)
dis[i]=INF;
dis[s]=0;
q.push(s);
while(!q.empty())
{
int u=q.top();
q.pop();
for(int i=0;i<G[u].size();i++)
{
int v=G[u][i].to;
if(dis[u]+G[u][i].cost<dis[v])
{
dis[v]=dis[u]+G[u][i].cost;
q.push(v);
}
}
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>m>>s;
int u,v,w;
for(int i=0;i<m;i++)
{
cin>>u>>v>>w;
G[u-1].push_back(edge(v-1,w));
}
SPFA(s-1);
for(int i=0;i<n;i++)
cout<<(dis[i]==INF?URA:dis[i])<<" ";
return 0;
}
SPFA判断带负环的图
例题:洛谷-【模板】负环
有两种思路:
1.队列SPFA中,若点入队次数>=n次,则存在负环。
这个结论怎么来的呢,思考了很久 ,考虑某个点,不计初始时源点入队那次,我们需要观察结点的入队条件:需要进行松弛操作且不在当前队列中,也就是说,结点在入队前成为了源点到某点的最短路径中的一点,而根据最短路径的性质,同名结点只能出现一次,所以该点最多在其他n-1个点的最多路径中都出现一次,即最多为n-1次。若>=n,说明该点在某条最短路径至少出现两次,该路径必存在环,进行了多余的松弛,也就说明存在负环。
2.根据最短路径的性质,有n个顶点的图的一条最短路径最多含有n-1条边,最多经过n个点,所以只需要判断某条最短路径是否超过了n个点或者超过n-1条边,是则判断含负环。具体操作为初始化源点cnt为1,对于每次松弛操作dis+w(u,v),说明源点到点v的最短路径经过点u,需要增加一条边,使cnt[v]=cnt[u]+1,随后判断cnt[v]这条路径上的边数是否>=n。
方法1在入队次数多,权值大时有爆int风险,而且一般方法2更快,这里给出方法2模板,采用判断最短路径所经过的边数是否>=n:
#include<iostream>
#include<queue>
#include<vector>
#include<stack>
#include<string.h>
using namespace std;
const int URA=(2<<30)-1;
const int MAX_N=2e3+10;
const int INF=1e9+10;
struct edge
{
int to,cost;
edge(int t,int c):to(t),cost(c){}
};
int t,n,m,s;
int dis[MAX_N];
vector<edge> G[MAX_N];
bool vis[MAX_N];
int cnt[MAX_N];//入队次数
bool SPFA(int s)//true说明有负环
{
queue<int> q;
for(int i=0;i<n;i++)
dis[i]=INF;
dis[s]=0;
q.push(s);
while(!q.empty())
{
int u=q.front();
q.pop();
vis[u]=false;//出队,但可以重新进入
for(int i=0;i<G[u].size();i++)
{
int v=G[u][i].to;
if(dis[u]+G[u][i].cost<dis[v])
{
cnt[v]=cnt[u]+1;
if(cnt[v]>=n)//有负环
return true;
dis[v]=dis[u]+G[u][i].cost;
if(!vis[v])//不在队列中
{
vis[v]=true;
q.push(v);
}
}
}
}
return false;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>t;
while(t--)
{
cin>>n>>m;
int u,v,w;
while(m--)
{
cin>>u>>v>>w;
if(w>=0)
{
G[u-1].push_back(edge(v-1,w));
G[v-1].push_back(edge(u-1,w));
}
else
G[u-1].push_back(edge(v-1,w));
}
if(SPFA(0))
cout<<"YES"<<"\n";
else
cout<<"NO"<<"\n";
memset(vis,0,sizeof(vis));
memset(cnt,0,sizeof(cnt));
memset(dis,0,sizeof(dis));
for(int i=0;i<n;i++)
G[i].clear();
}
return 0;
}