一、问题引入
在图论中,单源最短路径问题是最基础也最重要的问题之一。Dijkstra算法虽然高效,但它无法处理负权边。Bellman-Ford算法可以处理负权边,但时间复杂度高达O(VE)。
SPFA(Shortest Path Faster Algorithm) 是对Bellman-Ford算法的队列优化版本。它利用一个队列来维护需要松弛的顶点,平均情况下效率接近Dijkstra,同时保留了处理负权边和检测负环的能力。
二、算法原理
2.1 核心思想
SPFA的基本观察是:只有当一个顶点的最短距离被更新时,从它出发的边才可能引起其他顶点的距离更新。因此,我们不需要像Bellman-Ford那样每次都遍历所有边,而是只处理那些”距离发生变化”的顶点。
2.2 算法流程
- 初始化:源点距离为0,其他点为无穷大
- 将源点加入队列
- 从队列中取出一个顶点u
- 遍历u的所有邻接边,尝试松弛(即更新邻接点的最短距离)
- 如果某个邻接点v的距离被更新且v不在队列中,将v入队
- 重复步骤3-5直到队列为空
2.3 负环检测
如果一个顶点被松弛超过V次,说明图中存在负权回路。因为最短路径最多包含V-1条边,超过这个次数就意味着可以无限减小距离。
三、Java代码实现
import java.util.*;
/**
* SPFA最短路径算法实现
* 支持负权边和负环检测
*/
public class SPFA {
// 图的邻接表表示
private List<List<Edge>> graph;
private int numVertices;
/**
* 边类
*/
static class Edge {
int to; // 目标顶点
int weight; // 边权重
Edge(int to, int weight) {
this.to = to;
this.weight = weight;
}
}
/**
* 初始化图
* @param numVertices 顶点数量
*/
public SPFA(int numVertices) {
this.numVertices = numVertices;
this.graph = new ArrayList<>();
for (int i = 0; i < numVertices; i++) {
graph.add(new ArrayList<>());
}
}
/**
* 添加有向边
* @param from 起点
* @param to 终点
* @param weight 权重
*/
public void addEdge(int from, int to, int weight) {
graph.get(from).add(new Edge(to, weight));
}
/**
* SPFA算法求单源最短路径
* 时间复杂度:平均O(E),最坏O(VE)
* 空间复杂度:O(V + E)
*
* @param source 源点
* @return 距离数组,如果存在负环返回null
*/
public int[] shortestPath(int source) {
int[] dist = new int[numVertices];
boolean[] inQueue = new boolean[numVertices];
int[] count = new int[numVertices]; // 记录每个顶点入队次数
// 初始化距离
Arrays.fill(dist, Integer.MAX_VALUE);
dist[source] = 0;
// 使用LinkedList作为队列
Queue<Integer> queue = new LinkedList<>();
queue.offer(source);
inQueue[source] = true;
count[source] = 1;
while (!queue.isEmpty()) {
int u = queue.poll();
inQueue[u] = false;
// 遍历u的所有邻接边
for (Edge edge : graph.get(u)) {
int v = edge.to;
int weight = edge.weight;
// 松弛操作:如果经过u到v更短,则更新
if (dist[u] != Integer.MAX_VALUE && dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
// 如果v不在队列中,将其入队
if (!inQueue[v]) {
queue.offer(v);
inQueue[v] = true;
count[v]++;
// 负环检测:如果某个顶点入队超过V次,存在负环
if (count[v] > numVertices) {
System.out.println("检测到负权回路!");
return null;
}
}
}
}
}
return dist;
}
/**
* SPFA算法的SLF优化(Small Label First)
* 如果待入队顶点的距离小于队首,则插入队首
*/
public int[] shortestPathSLF(int source) {
int[] dist = new int[numVertices];
boolean[] inQueue = new boolean[numVertices];
int[] count = new int[numVertices];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[source] = 0;
Deque<Integer> deque = new LinkedList<>();
deque.offerFirst(source);
inQueue[source] = true;
count[source] = 1;
while (!deque.isEmpty()) {
int u = deque.pollFirst();
inQueue[u] = false;
for (Edge edge : graph.get(u)) {
int v = edge.to;
int weight = edge.weight;
if (dist[u] != Integer.MAX_VALUE && dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
if (!inQueue[v]) {
// SLF优化:小距离放队首
if (!deque.isEmpty() && dist[v] < dist[deque.peekFirst()]) {
deque.offerFirst(v);
} else {
deque.offerLast(v);
}
inQueue[v] = true;
count[v]++;
if (count[v] > numVertices) {
System.out.println("检测到负权回路!");
return null;
}
}
}
}
}
return dist;
}
/**
* 打印最短路径结果
*/
public void printResult(int[] dist, int source) {
if (dist == null) return;
System.out.println("从顶点 " + source + " 出发的最短距离:");
for (int i = 0; i < numVertices; i++) {
if (dist[i] == Integer.MAX_VALUE) {
System.out.println("到顶点 " + i + ": 不可达");
} else {
System.out.println("到顶点 " + i + ": " + dist[i]);
}
}
}
public static void main(String[] args) {
// 创建一个包含负权边的图
SPFA spfa = new SPFA(6);
// 添加边 (from, to, weight)
spfa.addEdge(0, 1, 5);
spfa.addEdge(0, 2, 3);
spfa.addEdge(1, 3, 6);
spfa.addEdge(1, 2, 2);
spfa.addEdge(2, 4, 4);
spfa.addEdge(2, 5, 2);
spfa.addEdge(2, 3, 7);
spfa.addEdge(3, 4, -1); // 负权边
spfa.addEdge(3, 5, 1);
spfa.addEdge(4, 5, -2); // 负权边
System.out.println("=== 标准SPFA ===");
int[] dist1 = spfa.shortestPath(0);
spfa.printResult(dist1, 0);
System.out.println("\n=== SPFA with SLF优化 ===");
int[] dist2 = spfa.shortestPathSLF(0);
spfa.printResult(dist2, 0);
// 测试负环检测
System.out.println("\n=== 负环检测测试 ===");
SPFA negativeCycleGraph = new SPFA(3);
negativeCycleGraph.addEdge(0, 1, 1);
negativeCycleGraph.addEdge(1, 2, -3);
negativeCycleGraph.addEdge(2, 0, 1); // 0->1->2->0 = -1,负环
int[] dist3 = negativeCycleGraph.shortestPath(0);
if (dist3 == null) {
System.out.println("正确检测到负环!");
}
}
}
四、复杂度分析
| 场景 | 时间复杂度 | 说明 |
|---|---|---|
| 平均情况 | O(E) | 每个顶点平均入队次数较少 |
| 最坏情况 | O(VE) | 与Bellman-Ford相同 |
| 负环检测 | O(VE) | 需要额外的count数组 |
五、算法优化技巧
- SLF优化:小距离顶点优先处理,减少无效松弛
- LLL优化:维护队列中距离的平均值,大于平均值则放队尾
- 堆优化:使用优先队列替代普通队列,但会丧失处理负权的能力
六、总结
SPFA算法是图论学习中承上启下的关键算法:
– 它比Dijkstra更通用(支持负权边)
– 它比Bellman-Ford更高效(平均情况下)
– 它引入了队列优化的重要思想
掌握SPFA后,读者可以更深入地理解最短路径算法的本质差异,以及不同优化策略对实际性能的影响。