SPFA 最短路
💡 核心思想
SPFA(Shortest Path Faster Algorithm)是 Bellman-Ford 的队列优化版本。它维护一个队列,只对"上次松弛成功的节点"的邻居进行松弛。可以处理负权边,也能检测负环。但最坏时间复杂度为 $O(VE)$。
SPFA 在实际中通常很快,但理论上可以被特殊数据卡到 $O(VE)$。NOI2018 中曾有一道题专门卡 SPFA,从此"SPFA 已死"成为名言。但在 CSP 竞赛中,如果图有负权边或需要判负环,SPFA 仍然是首选。
🎯 直觉理解
SPFA 就像"有消息才传播"。Dijkstra 是所有节点都关注,SPFA 是只有被更新的节点才通知邻居。如果一条消息在圈里传播了 $n$ 次还没停,说明有负环(消息越传越"好",永远不会停)。
📝 算法流程
- 初始化 dist[s]=0,其余 INF,s 入队
- 队首出队 u,对所有出边松弛
- 松弛成功且目标不在队中则入队
- 判负环:某节点入队次数超过 n 则有负环
$$dist[v] = \min(dist[v], dist[u] + w(u,v))$$
📊 复杂度分析
| 指标 | 复杂度 |
|---|---|
| 时间 | 平均 $O(kE)$(k 很小),最坏 $O(VE)$ |
| 空间 | $O(V+E)$ |
💻 参考实现(C++)
C++ (C++17)
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, m, s; cin >> n >> m >> s;
vector<vector<pair<int,long long>>> adj(n+1);
for (int i = 0; i < m; i++) {
int u, v; long long w; cin >> u >> v >> w;
adj[u].push_back({v, w});
}
const long long INF = 1e18;
vector<long long> dist(n+1, INF);
vector<bool> inq(n+1, false);
vector<int> cnt(n+1, 0);
queue<int> q;
dist[s] = 0; q.push(s); inq[s] = true;
while (!q.empty()) {
int u = q.front(); q.pop(); inq[u] = false;
for (auto [v, w] : adj[u]) {
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
if (!inq[v]) { q.push(v); inq[v] = true; if(++cnt[v]>n){cout<<"负环";return 0;} }
}
}
}
for (int i = 1; i <= n; i++) cout << dist[i] << " ";
return 0;
}⚠️ 常见坑点
被卡到 $O(VE)$ 时考虑换成 Dijkstra 或加优化
判负环时 cnt 数组要统计入队次数而非松弛次数
SLF 优化(新距离比队首小时放队首)可以加速
负环判断要在入队时做
📚 相关题目
| 题目 | 来源 | 难度 | 备注 |
|---|---|---|---|
| P3385 负环 | 洛谷 | CSP-S | SPFA判负环 |
| P3371 单源最短路径弱化版 | 洛谷 | CSP-J | SPFA可过 |