ALGO-5 最短路 — 单源最短路 SPFA 算法(java)

时间:2021-12-01 12:09:34

ALGO-5 最短路
问题描述

给定一个n个顶点,m条边的有向图(其中某些边权可能为负,但保证没有负环)。请你计算从1号点到其他点的最短路(顶点从1到n编号)。

输入格式

第一行两个整数n, m。

接下来的m行,每行有三个整数u, v, l,表示u到v有一条长度为l的边。

输出格式
共n-1行,第i行表示1号点到i+1号点的最短路。
样例输入
3 3
1 2 -1
2 3 -1
3 1 2
样例输出
-1
-2
数据规模与约定

对于10%的数据,n = 2,m = 2。

对于30%的数据,n <= 5,m <= 10。

对于100%的数据,1 <= n <= 20000,1 <= m <= 200000,-10000 <= l <= 10000,保证从任意顶点都能到达其他所有顶点。


题目分析:带负权边的单源最短路问题

算法分析:

题目的锦囊提示可以使用带堆优化的Dijkstra算法,因为此图的边数比点数的平方要少很多。但是根据题意,含负权边并且不含负环,这里我选用的是SPFA算法(Shortest Path Faster Algorithm)。SPFA是队列优化的Bellman-Ford算法,利用了队列的先进先出性质。按照本题的要求提一下思路:引入数组d [ ] ,d [ i ] 表示始点到 i 的最短路,初始赋值为无穷大,只有 始点的对应值为 0;引入数组 first [ ] ,记录前驱,初始赋值为-1,表示还没有知道前驱。接着将始点入队,每次读取一个入队元素并将其赋值给x,然后出队,并对所有相邻点进行松弛操作,松弛成功的点将其入队。 重复这个步骤,直到队列为空时算法结束。另外需要注意的是需要引入 boolean数组 vis [ ] ,用来标记点的状态,在读取、赋值、出队操作后要清除标记,表示该点已不在队列中,在入队操作后要添加标记。

关于松弛操作不太清楚的建议看一看算法书,或者根据给出的思路结合下面的代码再仔细思考一下。原理就是,依次枚举从u出发的边 u —> v,边的长度为len(题目中为l,这里为了看的清楚先写成len),判断d [ u ] + len 是否小于 d [ v ],如果小于则改进 d [ v ] 。

另外提一下的是,SPFA无法处理带负环的图,如果某个点进入队列的次数超过 n 次则存在负环。

算法设计:

import java.io.*;
import java.util.*;

class Main {
	static int n, m; // n 个顶点,m 条边
	static int[] u; // 始点
	static int[] v; // 终点
	static int[] l; // u 到 v 有一条长度为l的边
	static int[] d; // d[i]表示始点到 i 的最短路
	static int[] first; // 记录前驱
	static int[] next; // 赋值为-1,作为循环终止
	static boolean[] vis; // 标记状态
	static Queue<Integer> q = new LinkedList<Integer>(); //队列

	public static void main(String[] args) throws IOException {
		int i;
		BufferedReader bfr = new BufferedReader(new InputStreamReader(System.in)); //这里如果用scanner会运行超时
		String str = bfr.readLine();
		String[] s = str.split(" ");
		n = Integer.parseInt(s[0]);
		m = Integer.parseInt(s[1]);

		u = new int[m + 1];
		v = new int[m + 1];
		l = new int[m + 1];
		first = new int[n + 1];
		next = new int[m + 1];
		d = new int[n + 1];
		vis = new boolean[n + 1];
		for (i = 1; i <= n; i++) {
			first[i] = -1; // 前驱全部赋值为-1,表示还没有知道前驱
		}
		for (i = 1; i <= m; i++) {
			str = bfr.readLine();
			s = str.split(" ");
			u[i] = Integer.parseInt(s[0]);
			v[i] = Integer.parseInt(s[1]);
			l[i] = Integer.parseInt(s[2]);
			next[i] = first[u[i]]; // next[] 全部赋值为-1
			first[u[i]] = i; //存放 n 个结点
		}

		spfa(1);
		for (i = 2; i <= n; i++)
			System.out.println(d[i]);
	}

	public static void spfa(int s) {
		int i, x;
		for (i = 2; i <= n; i++) {
			d[i] = Integer.MAX_VALUE; // 除始点外,赋值为无穷大
		}
		q.offer(s); // 添加队头结点
		while (!q.isEmpty()) { // 操作直到队空为止
			x = q.poll(); // 读取队头结点s,赋值给x,并将s出队
			vis[x] = false; // 清除标记,表示不在队列中
			for (i = first[x]; i != -1; i = next[i]) {
				// 对相邻的点做松弛操作
				if (d[v[i]] > d[x] + l[i]) {
					d[v[i]] = d[x] + l[i];
					if (!vis[v[i]]) {
						vis[v[i]] = true; // 标记,表示在队列中
						q.offer(v[i]);// 入队
					}
				}
			}
		}
	}
}