哈喽大家好!今天我们来详细吃透图论中SPFA最短路径算法。
在图论最短路径问题中,我们最常用的两个算法:
-
Dijkstra :效率极高,但无法处理负权边,更无法处理负环
-
Bellman-Ford:可以处理负权边、判定负环,但时间复杂度极高 O(nm),大数据直接超时
而 SPFA(Shortest Path Faster Algorithm) 就是 Bellman-Ford 的队列优化版本 ,完美兼顾二者优势:支持负权边、能判负环、效率远高于朴素BF,是竞赛、刷题中处理负权最短路的首选算法。
本文将从零讲解SPFA原理、执行流程、手写C++模板、负环判定、常用优化、常见坑点,看完直接掌握可直接套用的工业级代码!
一、SPFA 前置知识:为什么需要它?
1.1 朴素 Bellman-Ford 的弊端
Bellman-Ford 的核心思路是:对所有边进行 n-1 轮松弛操作(n 为节点数),保证得到单源最短路径。
但它有一个致命问题:每一轮都会无脑遍历所有边,但大部分节点的距离根本没有更新,属于无效遍历,极大浪费时间。
1.2 SPFA 的核心优化思想
SPFA 基于一个关键结论:只有被更新过距离的节点,它的出边才有可能松弛其他节点。
所以 SPFA 放弃了暴力遍历所有边,改用队列存储所有「距离被更新、需要松弛邻边」的节点,只处理有效节点,大幅减少无效计算,效率飙升。
1.3 算法适用场景
-
求解带负权边的单源最短路径问题
-
判定图中是否存在负权环(负环)
-
稠密图、稀疏图均可,稀疏图效率最优(平均复杂度接近线性)
⚠️ 注意:若图存在负环,源点可达负环时,最短路径不存在(可以无限松弛变短)
二、核心概念:松弛操作
松弛操作是所有最短路算法的核心,SPFA 也不例外。
对于一条边 (u,v,w)(从u到v,权值为w):
如果满足:dist\[v\] \> dist\[u\] + w
说明:从源点经过 u 走到 v 的路径,比当前记录的 v 的最短路径更短。
此时更新:dist\[v\] = dist\[u\] + w,并将 v 加入队列,用于后续松弛其邻边。
三、SPFA 完整执行流程
我们以邻接表存储图(最常用、效率最高)为例,梳理完整步骤:
步骤1:初始化
-
距离数组
dist[]全部赋值为无穷大INF -
源点距离置0:
dist[s] = 0 -
队列清空,源点入队
-
标记数组
vis[]记录节点是否在队列中(防止重复入队)
步骤2:循环松弛
-
取出队首节点
u,标记出队 -
遍历
u的所有邻边(u, v, w) -
若可以松弛(更短路径),更新
dist[v] -
若
v不在队列中,将其入队,等待后续松弛
步骤3:终止条件
队列为空时,所有可松弛的路径全部更新完毕,算法结束,dist[] 即为源点到各点的最短路径。
四、C++ 完整基础代码实现(最短路模板)
这里提供竞赛通用模板,采用链式前向星存图,代码简洁、高效、可直接套用,带详细注释。
cpp
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
const int N = 100010; // 最大节点数
const int M = 200010; // 最大边数
const int INF = 0x3f3f3f3f;
// 链式前向星存图
int h[N], e[M], w[M], ne[M], idx;
int dist[N]; // 存储源点到各点最短距离
bool vis[N]; // 标记节点是否在队列中
// 加边函数:a->b 权值c
void add(int a, int b, int c)
{
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx++;
}
// SPFA核心函数:s为源点
void spfa(int s)
{
// 初始化
memset(dist, 0x3f, sizeof dist);
memset(vis, false, sizeof vis);
queue<int> q;
dist[s] = 0;
q.push(s);
vis[s] = true;
while (!q.empty())
{
int u = q.front();
q.pop();
vis[u] = false; // 出队,取消标记
// 遍历所有邻边
for (int i = h[u]; i != -1; i = ne[i])
{
int v = e[i];
int weight = w[i];
// 松弛操作
if (dist[v] > dist[u] + weight)
{
dist[v] = dist[u] + weight;
// 不在队列则入队
if (!vis[v])
{
q.push(v);
vis[v] = true;
}
}
}
}
}
int main()
{
// 初始化表头
memset(h, -1, sizeof h);
int n, m;
cin >> n >> m;
// 读入m条边
for (int i = 0; i < m; i++)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
// 以1号节点为源点跑最短路
spfa(1);
// 输出结果
for (int i = 1; i <= n; i++)
{
if (dist[i] == INF) cout << "无法到达 ";
else cout << dist[i] << " ";
}
return 0;
}
代码关键点解析
-
vis数组的作用:避免同一个节点重复入队,防止队列冗余、超时
-
0x3f3f3f3f:常用无穷大值,相加不会溢出,适配int范围
-
链式前向星:相比vector邻接表,速度更快,适配大数据图论题目
五、SPFA 进阶:负权环判定
SPFA 最核心的进阶功能就是判断图中是否存在负环,这是Dijkstra无法做到的。
5.1 判定原理
根据图论定理:n-1一个不含负环的图,任意节点的最短路径最多经过 条边(n为节点数)。
如果某个节点n-1入队次数超过 次 ,说明该节点被无限松弛,图中存在源点可达的负环。
5.2 负环判定完整代码
cpp
int cnt[N]; // 记录每个节点入队次数
// 返回true表示存在负环
bool spfa_check(int s, int n)
{
memset(dist, 0x3f, sizeof dist);
memset(vis, false, sizeof vis);
memset(cnt, 0, sizeof cnt);
queue<int> q;
dist[s] = 0;
q.push(s);
vis[s] = true;
cnt[s] = 1;
while (!q.empty())
{
int u = q.front();
q.pop();
vis[u] = false;
for (int i = h[u]; i != -1; i = ne[i])
{
int v = e[i];
int weight = w[i];
if (dist[v] > dist[u] + weight)
{
dist[v] = dist[u] + weight;
if (!vis[v])
{
q.push(v);
vis[v] = true;
cnt[v]++;
// 入队次数超过n-1,存在负环
if (cnt[v] > n) return true;
}
}
}
}
return false;
}
5.3 全域负环判定技巧
如果需要判断整张图是否存在负环(不限源点),无需枚举所有源点,只需:
-
新建一个超级源点
0 -
向所有节点连一条权值为0的边
-
以0为源点跑SPFA,即可检测全图负环
六、时间复杂度与性能分析
6.1 复杂度
-
平均时间复杂度 :
(m为边数),效率极高,日常刷题完全够用
-
最坏时间复杂度 :
(极端卡数据场景,退化为朴素BF)
6.2 常用优化(防卡常)
竞赛中部分题目会卡普通SPFA,可加入两种经典优化,通过率拉满:
-
SLF优化(小根队列):用双端队列,新节点距离小于队首则插入队首,否则插队尾,减少松弛次数
-
LLL优化:维护队列平均距离,优先松弛距离更小的节点
日常刷题SLF优化足够用,实现简单、提升显著。
七、新手常见坑点(重点避坑)
-
坑1:忘记vis数组标记:导致节点重复入队,队列爆炸超时
-
坑2:INF取值不当 :INF过大会溢出,过小会被误松弛,推荐
0x3f3f3f3f -
坑3:负环判断条件写错 :判定条件是
cnt[v] > n,不是n-1 -
坑4:无清空数组:多组测试数据时,未清空h、dist、vis数组,导致数据污染
-
坑5:混淆有向图/无向图:无向图需要双向加边,有向图只需单向加边
八、SPFA vs Dijkstra vs Bellman-Ford 对比
| 算法 | 能否处理负权 | 能否判负环 | 平均复杂度 | 适用场景 |
|---|---|---|---|---|
| Dijkstra(讲过了自己看哦) | 否 | 否 | 正权图最短路 | |
| Bellman-Ford(下一篇博客讲解) | 是 | 是 | 小规模负权图 | |
| SPFA | 是 | 是 | 负权图、判负环(通用首选) |
九、总结
-
SPFA 是 Bellman-Ford 的队列优化版,核心是只松弛有效更新节点,规避无效计算;
-
核心能力:处理负权边、判定负环,是正权图下Dijkstra的完美补充;
-
代码模板可直接套用,日常刷题、竞赛、面试均高频考察;
-
存在极端卡数据场景,可搭配SLF优化进一步提升稳定性。
后续遇到负权最短路、负环判定问题,直接锁死 SPFA 即可!