题目链接:单源最短路
突然发现以前没写过spfa
dijstra传送门
SPFA算法核心思想
SPFA(Shortest Path Faster Algorithm) 是Bellman-Ford算法的队列优化版本,用于求解单源最短路径问题,特别适合处理有负权边但无负环的图。
算法步骤:
-
初始化
- 所有节点距离初始化为无穷大(
INT_MAX) - 源点距离设为0,加入队列
- 所有节点距离初始化为无穷大(
-
队列处理
- 从队列取出节点u
- 遍历u的所有邻接边(u,v)
- 如果
dis[v] > dis[u] + w(u,v),则更新dis[v] - 如果v不在队列中且被更新了,将v加入队列
-
重复直到队列为空
核心代码:
void spfa(int x){
for(int i=1;i<=n;i++) dis[i]=INT_MAX; // 初始化距离
ex[x]=1;ext[1]=x;dis[x]=0;int r=1,h=1; // 源点入队
while(r<=h){ // 队列不为空
int temp=head[ext[r]]; // 当前节点的第一条边
ex[ext[r]]=0; // 标记出队
while(1){ // 遍历所有邻接边
int v=to[temp];
if(dis[v]>dis[ext[r]]+lon[temp]){ // 松弛操作
dis[v]=dis[ext[r]]+lon[temp];
if(!ex[v]){ // 如果不在队列中
ext[++h]=v; // 入队
ex[v]=1;
num[v]++; // 记录入队次数
}
if(num[v]>n) return; // 检测负环
}
if(link[temp]) temp=link[temp]; // 下一条边
else break;
}
r++; // 处理下一个节点
}
}
关键特点:
- 数据结构:使用链式前向星存图
- 负环检测:`num[v] > n 时认为存在负环
- 优化:只有被更新的节点才重新入队
突然发现2018年写的代码现在过不了洛谷评测了,原因是link不能做变量名了
这就是SPFA的精髓:通过队列避免不必要的松弛操作,比朴素Bellman-Ford效率更高!
2018版本
#include<bits/stdc++.h>
using namespace std;
int n,m,s;
int e=0,dis[110001],to[1500001],link[1500001],head[110001],lon[1500001];
void put(int x,int y,int z){
to[++e]=y;
lon[e]=z;
link[e]=head[x];
head[x]=e;
}
bool ex[10001];int ext[600001],num[10001];
void spfa(int x){
for(int i=1;i<=n;i++) dis[i]=INT_MAX;
ex[x]=1;ext[1]=x;dis[x]=0;int r=1,h=1;
while(r<=h){
int temp=head[ext[r]];
ex[ext[r]]=0;
while(1){
int v=to[temp];
if(dis[v]>dis[ext[r]]+lon[temp]){
dis[v]=dis[ext[r]]+lon[temp];
if(!ex[v]){
ext[++h]=v;
ex[v]=1;
num[v]++;
}
if(num[v]>n) return;
}
if(link[temp]) temp=link[temp];
else break;
}
r++;
}
}
int main(){
cin>>n>>m>>s;
for(int i=1;i<=m;i++){
int u,v,l;
cin>>u>>v>>l;
put(u,v,l);
}
spfa(s);
for(int i=1;i<=n;i++){
cout<<dis[i]<<" ";
}
//cheak code
//cout<<endl;
//cout<<INT_MAX;
}

2025版本
#include<bits/stdc++.h>
using namespace std;
int n,m,s;
int e=0,dis[110001],to[1500001],nxt[1500001],head[110001],lon[1500001]; // 将link改为nxt
void put(int x,int y,int z){
to[++e]=y;
lon[e]=z;
nxt[e]=head[x]; // 改为nxt
head[x]=e;
}
bool ex[110001]; // 大小改为110001
int ext[600001],num[110001]; // 大小改为110001
void spfa(int x){
for(int i=1;i<=n;i++) dis[i]=INT_MAX;
memset(ex,0,sizeof(ex));
memset(num,0,sizeof(num));
ex[x]=1;
ext[1]=x;
dis[x]=0;
int r=1,h=1;
while(r<=h){
int u=ext[r];
ex[u]=0;
for(int temp=head[u];temp;temp=nxt[temp]){ // 改为nxt,使用for循环遍历
int v=to[temp];
if(dis[v]>dis[u]+lon[temp]){
dis[v]=dis[u]+lon[temp];
if(!ex[v]){
ext[++h]=v;
ex[v]=1;
num[v]++;
if(num[v]>n) return; // 通常判断负环用n而不是m
}
}
}
r++;
}
}
int main(){
cin>>n>>m>>s;
for(int i=1;i<=m;i++){
int u,v,l;
cin>>u>>v>>l;
put(u,v,l);
}
spfa(s);
for(int i=1;i<=n;i++){
cout<<dis[i]<<" ";
}
return 0;
}
SPFA时间复杂度分析
理论时间复杂度
最坏情况:O(VE),其中V是节点数,E是边数
- 每个节点可能入队V次
- 每次出队需要遍历所有邻接边
平均情况:O(kE),其中k是常数,通常k≈2
- 在实际应用中表现很好
- 大多数节点只入队1-2次
最好情况:O(E)
- 类似BFS,每个节点只入队一次
代码具体分析
while(r<=h){ // 外层循环:最多O(V)次
int temp=head[ext[r]];
ex[ext[r]]=0;
while(1){ // 内层循环:遍历所有邻接边,O(deg(v))
// ...松弛操作...
if(link[temp]) temp=link[temp];
else break;
}
r++;
}
总复杂度:Σ(deg(v) × 入队次数) = O(VE)
与相关算法对比
| 算法 | 时间复杂度 | 适用场景 |
|---|---|---|
| Dijkstra | O((V+E)logV) | 无负权边 |
| Bellman-Ford | O(VE) | 有负权边 |
| SPFA | 平均O(kE),最坏O(VE) | 有负权边,稀疏图 |
| Floyd | O(V³) | 多源最短路径 |
实际性能特点
- 稀疏图表现优异:在边数E ≈ V的情况下接近O(V)
- 稠密图退化:当E ≈ V²时,退化为O(V³)
- 对负环敏感:存在负环时可能陷入死循环(你的代码有检测机制)
- 常数因子小:实际运行通常比理论分析要好
总结:SPFA在竞赛和实际应用中很受欢迎,因为对于随机图它的平均时间复杂度接近O(E),比Dijkstra的O(ElogV)常数更小,还能处理负权边。
560

被折叠的 条评论
为什么被折叠?



