给定一个n个顶点,m条边的有向图(其中某些边权可能为负,但保证没有负环)。请你计算从1号点到其他点的最短路(顶点从1到n编号)。
第一行两个整数n, m。
接下来的m行,每行有三个整数u, v, l,表示u到v有一条长度为l的边。
1 2 -1
2 3 -1
3 1 2
-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]);// 入队 } } } } } }