@[toc]
一、前言
SPFA算法,全称为Shortest Path Faster Algorithm,是求解单源最短路径问题的一种常用算法,它可以处理有向图或者无向图,边权可以是正数、负数,但是不能有负环。
二、SPFA 算法
1、SPFA算法的基本流程
1. 初始化
首先我们需要起点s到其他顶点的距离初始化为一个很大的值(比如9999999,像是 JAVA 中可以设置 Integer.MAX_VALUE
来使),并将起点s的距离初始化为0。同时,我们还需要将起点s入队。
2. 迭代
每次从队列中取出一个顶点u,遍历所有从u出发的边,对于边(u,v)(其中v为从u可以到达的顶点),如果s->u->v的路径长度小于s->v的路径长度,那么我们就更新s->v的路径长度,并将v入队。
3. 循环
不断进行步骤2,直到队列为空。
4. 判断
最后,我们可以得到从起点s到各个顶点的最短路径长度,如果存在无穷小的距离,则说明从起点s无法到达该顶点。
其次,需要注意的是,SPFA算法中存在负环问题。如果存在负环,则算法会陷入死循环。因此,我们需要添加一个计数器,记录每个点进队列的次数。当一个点进队列的次数超过图中节点个数时,就可以判定存在负环。
2、代码详解
以下是使用Java实现 SPFA算法的代码,其中Graph类表示有向图或无向图,Vertex类表示图中的一个顶点,Edge类表示图中的一条边。
import java.util.*;
class Graph {
private List<Vertex> vertices;
public Graph() {
vertices = new ArrayList<Vertex>();
}
public void addVertex(Vertex v) {
vertices.add(v);
}
public List<Vertex> getVertices() {
return vertices;
}
}
class Vertex {
private int id;
private List<Edge> edges;
private int distance;
private boolean visited;
public Vertex(int id) {
this.id = id;
edges = new ArrayList<Edge>();
distance = Integer.MAX_VALUE;
visited = false;
}
public int getId() {
return id;
}
public void addEdge(Edge e) {
edges.add(e);
}
public List<Edge> getEdges() {
return edges;
}
public int getDistance() {
return distance;
}
public void setDistance(int distance) {
this.distance = distance;
}
public boolean isVisited() {
return visited;
}
public void setVisited(boolean visited) {
this.visited = visited;
}
}
class Edge {
private Vertex source;
private Vertex destination;
private int weight;
public Edge(Vertex source, Vertex destination, int weight) {
this.source = source;
this.destination = destination;
this.weight = weight;
}
public Vertex getSource() {
return source;
}
public Vertex getDestination() {
return destination;
}
public int getWeight() {
return weight;
}
}
public class SPFA {
public static void spfa(Graph graph, Vertex source) {
Queue<Vertex> queue = new LinkedList<Vertex>();
for (Vertex v : graph.getVertices()) {
v.setDistance(Integer.MAX_VALUE);
v.setVisited(false);
}
source.setDistance(0);
queue.add(source);
int count = 0;
while (!queue.isEmpty()) {
Vertex u = queue.poll();
u.setVisited(false);
for (Edge e : u.getEdges()) {
Vertex v = e.getDestination();
int distance = u.getDistance() + e.getWeight();
if (distance < v.getDistance()) {
v.setDistance(distance);
if (!v.isVisited()) {
queue.add(v);
v.setVisited(true);
count++;
if (count > graph.getVertices().size()) {
throw new RuntimeException("Negative cycle detected");
}
}
}
}
}
}
public static void main(String[] args) {
Graph graph = new Graph();
Vertex s = new Vertex(0);
Vertex a = new Vertex(1);
Vertex b = new Vertex(2);
Vertex c = new Vertex(3);
Vertex d = new Vertex(4);
s.addEdge(new Edge(s, a, 2));
s.addEdge(new Edge(s, c, 1));
a.addEdge(new Edge(a, b, 3));
b.addEdge(new Edge(b, d, 2));
c.addEdge(new Edge(c, d, 1));
graph.addVertex(s);
graph.addVertex(a);
graph.addVertex(b);
graph.addVertex(c);
graph.addVertex(d);
spfa(graph, s);
for (Vertex v :graph.getVertices()) {
System.out.println("Shortest distance from source to vertex " + v.getId() + " is " + v.getDistance());
}
}
}
上面的代码实现了SPFA算法,并计算了从给定源点到图中其他所有顶点的最短路径。主要思路如下:
- 初始化:将所有顶点的距离设置为正无穷,将源点的距离设置为0,将源点加入队列。
- 迭代:从队列中取出一个顶点u,遍历它的所有邻居v。如果u到源点的距离加上u到v的边的权重小于v的距离,则更新v的距离,并将v加入队列中。如果v已经在队列中,则不需要再次添加。
- 如果队列为空,则算法结束。如果队列非空,则回到步骤2。
SPFA算法的时间复杂度取决于负权边的数量。如果图中没有负权边,算法的时间复杂度是O(E),其中E是边的数量。但是如果图中有负权边,算法的时间复杂度将达到O(VE),其中V是顶点的数量,E是边的数量。因此,为了避免算法的时间复杂度变得非常高,应尽可能避免在图中使用负权边。
三、SPFA 算法已死 ?
这个问题引发了很多OI选手和出题人的讨论,虽然 SPFA 算法在实际应用中具有一定的优势,但它也有一些缺点,导致它被称为"已死"的算法之一。以下是几个原因:
- 可能会进入负环:SPFA 算法可以处理负权边,但是如果有负权环,算法将无法结束,因为每次都会沿着负权环一遍一遍地更新距离,导致算法陷入死循环。
- 时间复杂度不稳定:在最坏情况下,SPFA 算法的时间复杂度可以达到 O(VE),其中 V 和 E 分别是图中的顶点数和边数。而在最好情况下,时间复杂度只有 O(E)。因此,SPFA 算法的时间复杂度是不稳定的。
- 存在更好的算法:对于单源最短路径问题,已经有更好的算法出现,如 Dijkstra 算法和 Bellman-Ford 算法。这些算法在时间复杂度和稳定性方面都比 SPFA 算法更优秀。
虽然 SPFA 算法在某些情况下可以发挥出优势,但是它的缺点也是无法忽视的,而且已经有更好的算法出现,不少大佬也或多或少的对 SPFA 算法进行了优化,更多优化的内容以及最短路径算法可以在论文中找到。因此,SPFA 算法已经不是首选算法,也可以说是已经“死亡”了。