1. 算法概述
SPFA(Shortest Path Faster Algorithm)算法是Bellman-Ford算法的队列优化版本,用于求解单源最短路径问题。
核心思想:
-
通过动态逼近法,不断松弛边来更新最短路径
-
使用队列优化,只对可能产生松弛操作的节点进行处理
特点:
-
可以处理带负权边的图
-
能够检测负权环
-
在稀疏图上效率较高
2. 算法适用条件
✔ 带权有向图或无向图
✔ 可以处理负权边的情况
✔ 需要检测负权环时特别有用
✖ 不能处理包含负权环的图的最短路径问题
3. 算法步骤
初始化阶段
memset(dist, 0x3f, sizeof dist); // 所有距离初始化为∞
dist[src] = 0; // 源点距离设为0
queue<int> q;
q.push(src); // 源点入队
st[src] = true; // 标记源点在队列中
主循环过程
while(!q.empty()) {
int t = q.front();
q.pop();
st[t] = false; // 标记t已出队
for(遍历t的所有邻接边){
if(可以松弛){
更新距离;
if(目标节点不在队列中){
入队;
标记在队列中;
}
}
}
}
4. 时间复杂度分析
情况 | 时间复杂度 | 说明 |
---|---|---|
平均情况 | O(E) | 在稀疏图上表现良好 |
最坏情况 | O(VE) | 退化为Bellman-Ford |
5. 代码模板(C++)
基础版本(求最短路)
const int N = 1e5 + 10;
int dist[N];
int h[N], e[N], ne[N], w[N], idx;
bool st[N];
int spfa(int src, int dest) {
memset(dist, 0x3f, sizeof dist);
dist[src] = 0;
queue<int> q;
q.push(src);
st[src] = true;
while(!q.empty()) {
int t = q.front();
q.pop();
st[t] = false;
for(int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if(dist[j] > dist[t] + w[i]) {
dist[j] = dist[t] + w[i];
if(!st[j]) {
q.push(j);
st[j] = true;
}
}
}
}
return dist[dest];
}
检测负权环版本
bool spfa() {
queue<int> q;
for(int i = 1; i <= n; i++) { // 所有节点入队
q.push(i);
st[i] = true;
}
while(!q.empty()) {
int t = q.front();
q.pop();
st[t] = false;
for(int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if(dist[j] > dist[t] + w[i]) {
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if(cnt[j] >= n) return true; // 存在负权环
if(!st[j]) {
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
6. 常见问题
Q1:SPFA和Dijkstra算法的区别?
-
Dijkstra不能处理负权边,SPFA可以
-
Dijkstra使用贪心策略,SPFA使用动态逼近
-
Dijkstra时间复杂度稳定,SPFA在特殊数据下可能退化
Q2:如何判断负权环?
-
维护一个cnt数组记录节点的入队次数
-
如果某个节点的cnt ≥ n,说明存在负权环
Q3:为什么SPFA能处理负权边?
-
它允许节点多次入队,可以重新处理被负权边影响的节点
-
不像Dijkstra那样一旦确定就不可更改
7. 典型例题
8. 优化技巧
-
使用双端队列(deque)代替普通队列
-
随机选择队首或队尾插入
-
设置最大松弛次数限制防止极端情况
9. 声明
以上笔记仅为学习该算法的学习笔记,如有错误请指出
总结:SPFA算法是处理带负权边图的最短路径问题的有效算法,理解其队列优化思想和负权环检测机制对掌握图论算法很重要。