【图论】Day02_最短路算法之「SPFA」

一、什么是 SPFA 算法 ?

  • SPFA 简介:SPFA 是队列优化的 bellman-ford 算法,bellman-ford 算法进行边松弛操作时,上一轮松弛中没有发生变化的顶点出发的边也会进行松弛,存在大量的冗余操作。
  • SPFA 算法对此情况进行了优化,只对发生变化的顶点出发的边进行松弛,如果所有顶点的距离都不再改变,算法结束。

特点

  • 能够计算正/负权图问题。而且效率比 Dijkstra 算法高。
  • O ( k E ) O(kE) 复杂度,即遍历所有的边。注:网格形状的稀疏图容易让 SPFA 算法变为 O ( E V ) O(EV) ,即 E E 约等于 2 V 2V
  • 能够判断是否有负环。推荐使用最坏 O ( E V ) O(EV) 的 SPFA,相比与 BF 算法的固定的 O ( E V ) O(EV) 好很多。

二、题解

方法一:邻接表

  • 初始化 d i s [ 1 ] dis[1] 为 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;
		}
	}
}

复杂度分析

  • 时间复杂度: O ( k E ) O(kE) ,k 为结点入队的平均次数。
  • 空间复杂度: O ( V + E ) O(V+E)

方法二:链式前向星

这道题 Java 用 spfa 是过不了的,只能最多能地 60 分。上面用集合存储更糟糕,只能得 30pt。更别说这道数据加强题

超时错误:听说是出题人用特殊数据将 spfa 卡成了 O ( E V ) O(EV)

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;
		}
	}
}

复杂度分析

  • 时间复杂度: O ( k E ) O(kE) ,最坏是 O ( E V ) O(EV)
  • 空间复杂度: O ( E + V ) O(E+V)

方法三: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

发布了691 篇原创文章 · 获赞 151 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_43539599/article/details/105478037