一、什么是 SPFA 算法 ?
- SPFA 简介:SPFA 是队列优化的 bellman-ford 算法,bellman-ford 算法进行边松弛操作时,上一轮松弛中没有发生变化的顶点出发的边也会进行松弛,存在大量的冗余操作。
- SPFA 算法对此情况进行了优化,只对发生变化的顶点出发的边进行松弛,如果所有顶点的距离都不再改变,算法结束。
特点
- 能够计算正/负权图问题。而且效率比 Dijkstra 算法高。
- 复杂度,即遍历所有的边。注:网格形状的稀疏图容易让 SPFA 算法变为 ,即 约等于
- 能够判断是否有负环。推荐使用最坏 的 SPFA,相比与 BF 算法的固定的 好很多。
二、题解
方法一:邻接表
- 初始化 为 0,其他为 ,源点入队,标记 vis[1] = true
- 从队头取出结点,遍历所有结点,对所有点进行松弛操作。
- 如果顶点 v 的最短路径估计值有所调整,且顶点 v 不在当前的队列中,添加到队列中去,即
add(edge.end)
- 注:一个点可能出队/入队多次,但不会同时在队列里出现。
import java.util.*;
import java.math.*;
import java.io.*;
public class Main{
static int V, E;
static int[] dist;
static boolean[] vis;
static List<Edge>[] graph;
static int INF = 0x3f3f3f3f;
private static List<Edge>[] createGraph(int V){
List<Edge>[] graph = new List[V+1];
for (int i = 0; i <= V; i++)
graph[i] = new ArrayList<>();
return graph;
}
private static void addEdge(int from, int to, int cost) {
graph[from].add(new Edge(to, cost));
}
private static int spfa(int S) {
Queue<Integer> q = new ArrayDeque<>();
q.offer(S);
vis[S] = true;
dist[S] = 0;
while (!q.isEmpty()) {
Integer cur = q.poll();
vis[cur] = false;
for (int i = 0; i < graph.length; i++)
for (Edge edge : graph[i]) {
if (dist[edge.to] > dist[i] + edge.cost) { //注:i为顶点编号。
dist[edge.to] = dist[i] + edge.cost;
if (!vis[edge.to]) {
q.offer(edge.to);
vis[edge.to] = true;
}
}
}
}
if (dist[V] == INF) return -1;
return dist[V];
}
public static void main(String[] args) throws IOException {
Scanner sc = new Scanner(new BufferedInputStream(System.in));
BufferedWriter w = new BufferedWriter(new OutputStreamWriter(System.out));
V = sc.nextInt();
E = sc.nextInt();
dist = new int[V+1];
vis = new boolean[V+1];
graph = createGraph(V+1);
Arrays.fill(dist, INF);
for (int i = 0; i < E; i++) {
int from = sc.nextInt();
int to = sc.nextInt();
int cost = sc.nextInt();
addEdge(from, to, cost);
}
int res = spfa(1);
System.out.println(res);
}
static class Edge {
int to, cost;
public Edge(int _to, int _cost) {
to = _to;
cost = _cost;
}
}
}
复杂度分析
- 时间复杂度: ,k 为结点入队的平均次数。
- 空间复杂度: ,
方法二:链式前向星
这道题 Java 用 spfa 是过不了的,只能最多能地 60 分。上面用集合存储更糟糕,只能得 30pt。更别说这道数据加强题了
超时错误:听说是出题人用特殊数据将 spfa 卡成了
import java.util.*;
import java.math.*;
import java.io.*;
public class Main{
static int V, E;
static int[] dist;
static int INF = 0x3f3f3f3f;
static boolean[] inq;
static int[] head;
static Edge[] edges;
static int tot;
static int MAXN = 1000000;
private static void addEdge(int u, int v, int w) {
edges[++tot] = new Edge();
edges[tot].to = v;
edges[tot].w = w;
edges[tot].next = head[u];
head[u] = tot;
}
private static void spfa(int S) {
dist[S] = 0;
Queue<Integer> q = new ArrayDeque<>();
q.add(S);
inq[S] = true;
while (!q.isEmpty()) {
int v = q.poll();
inq[v] = false;
for (int i = head[v]; i != 0; i = edges[i].next) {
int to = edges[i].to, w = edges[i].w;
if (dist[to] > dist[v] + w) {
dist[to] = dist[v] + w;
if (!inq[to]) {
q.add(to);
inq[to] = true;
}
}
}
}
}
public static void main(String[] args) throws IOException {
Scanner sc = new Scanner(new BufferedInputStream(System.in));
BufferedWriter w = new BufferedWriter(new OutputStreamWriter(System.out));
V = sc.nextInt();
E = sc.nextInt();
int s = sc.nextInt();
inq = new boolean[MAXN];
dist = new int[MAXN];
head = new int[MAXN];
edges = new Edge[MAXN];
Arrays.fill(dist, INF);
for (int i = 0; i < E; i++) {
int from = sc.nextInt();
int to = sc.nextInt();
int cost = sc.nextInt();
addEdge(from, to, cost);
}
spfa(s);
for (int i = 1; i <= V; i++) {
System.out.print(dist[i] + " ");
}
}
static class Edge {
int to, w, next;
public Edge() { }
public Edge(int to, int w, int next) {
this.to = to;
this.w = w;
this.next = next;
}
}
}
复杂度分析
- 时间复杂度: ,最坏是
- 空间复杂度: ,
方法三:SLF / LLF / SLF + LLF 优化
(1) SLF 优化
SLF 即 small label first,其原理是当加入一个新点 to 的时候如果此时的 dist[to]
比队首 dist[q.peek()]
还要小的话,就把结点 to 加入到队首,否则把他加入到队尾。证明不是很懂,可能是因为先扩展最小的点可以尽量使程序尽早的结束把。
SPFA 代码如下:
void spfa(int S) {
Arrays.fill(dist, INF);
dist[S] = 0;
ArrayDeque<Integer> q = new ArrayDeque<>();
q.add(S);
inq[S] = true;
while (!q.isEmpty()) {
int v = q.poll();
inq[v] = false;
for (int i = head[v]; i != 0; i = edges[i].next) {
int to = edges[i].to, w = edges[i].w;
if (dist[to] > dist[v] + w) {
dist[to] = dist[v] + w;
if (!inq[to]) {
if (!q.isEmpty() && dist[to] < dist[q.peek()]) {
q.addFirst(to);
} else {
q.addLast(to);
}
inq[to] = true;
}
}
}
}
}
(2) LLF 优化
LLF 即 large lable fist,原理是记录现在队列中元素所代表值的平均值 avg,和要压入元素的值相比较,如果大于平均值,直接压入对列尾部。
(3) SLF + LLF 优化
比 SLF 快一点,随缘把…
有关需要 SLF 优化才能 AC 的习题可以参考下面的习题:
https://www.acwing.com/problem/content/description/344/
文章参考链接
SPFA优化:https://blog.csdn.net/zhouchangyu1221/article/details/90549195
ArrayDeque 为什么快:https://www.cnblogs.com/lxyit/p/9080590.html