前言 最短路算法用来求图中两个点的最短路径。最短路又分为单源最短路、多源汇最短路。
定义:图中顶点数为$n$,边数为$m$。
单源最短路 单源最短路:从一个点到其他所有点的最短距离。根据权值分为两种情况:所有权值为正数、存在权值为负数。
所有权值为正数 Dijkstra算法 1.适用情况:适用于稠密图 ,$n^2$与$m$为一个级别。
2.时间复杂度:$O(n^2)$
3.图存储方式:邻接矩阵
4.算法描述:$g$为邻接矩阵,$dist$为每个点到源点的距离,集合$s$为已经确定最短路径的顶点集合。初始时,$dist$为正无穷,$s$包含源点。
a.设 $dist[1] = 0$,表示源点的距离确定为$0$。
b.从还没有确定的最短路径的点中,选取距离最小的点。
c.将该点加入到$S$中,用该点更新其它点的距离。
d.重复b和c,直到$S$中包含所有的顶点。
5.代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static int dijkstra(){ for(int i = 0; i < N; i++)dist[i] = 0x3f3f3f3f;//表示无穷大 dist[1] = 0; for(int i = 0; i < n; i++){ int t = -1; //t是还没有确定距离, 距离最近的点 for(int j = 1; j <= n; j++){ if(!st[j] && (t == -1 || dist[t] > dist[j]))t = j;//在没有确定的集合中,选一个距离最短的点 } st[t] = true; //用t来更新距离 for(int j = 1; j <= n; j++){ dist[j] = Math.min(dist[j], dist[t] + g[t][j]); } } if(dist[n] == 0x3f3f3f3f)return -1; return dist[n]; }
堆优化版的Dijkstra算法 1.适用情况:适用于稀疏图 ,$n$与$m$为一个级别。
2.时间复杂度:$O(mlogn)$
3.图存储方式:邻接表
4.算法描述:大体上与Dijkstra算法一致,区别是:堆优化版的Dijkstra算法对Dijkstra算法的b步骤进行优化,用优先队列模拟小根堆,根节点为距离最小的点,这一步优化后将$O(n^2)$的复杂度,降低到$O(n)$。
5.代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static int dijkstra(){ for(int i = 0; i < N; i++)dist[i] = 0x3f3f3f3f; dist[1] = 0; heap.offer(new int[]{1, 0}); //1号点,距离为0 while(!heap.isEmpty()){ int[] temp = heap.poll(); //小根堆,距离最小的点 int t = temp[0]; int distance = temp[1]; if(st[t])continue; st[t] = true; for(int i = h[t]; i != -1; i = ne[i]){ //稀疏图用邻接表存储 int j = e[i]; if(dist[j] > distance + w[i]){ dist[j] = distance + w[i]; heap.offer(new int[] {j, dist[j]}); } } } return dist[n] == 0x3f3f3f3f ? -1 : dist[n]; }
优先队列实现小根堆:
1 public static PriorityQueue<int[]> heap = new PriorityQueue<>(N, (a, b) -> {return a[1] - b[1];});
存在权值为负数 bellman-ford算法 1.适用情况:存在负权边,求$1$号点到$n$号点的最短路,最多经过$k$条边。
2.时间复杂度:$O(nm)$
3.图存储方式:存端点$(a, b)$和边权$w$
4.算法描述:对每条边都执行松弛操作($dist[b] = min(dist[b], dist[a] + w)$),共$k$次。
5.代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 class Main{ public static int N = 510, n, m, k, M = 10010; public static int[] dist = new int[N]; public static int[] backup = new int[N]; //为了防止串联效应, 用更新的点的距离又去更新其他的点 static class Edge{//创建一个边类,包含属性a, b, w int a, b, w; public Edge(int a, int b, int w){ this.a = a; this.b = b; this.w = w; } } public static Edge[] edge = new Edge[M];//有M条边, 创建Edge类型的数组 public static int bellman_ford(){ Arrays.fill(dist, 0x3f3f3f3f); dist[1] = 0; for(int i = 0; i < k; i++){ backup = dist.clone(); //更新前需要拷贝上一次的dist, 防止串联效应 for(int j = 0; j < m; j++){ int a = edge[j].a, b = edge[j].b, w = edge[j].w; dist[b] = Math.min(dist[b], backup[a] + w); //用上一次a的距离来更新 } } return dist[n] > 0x3f3f3f3f / 2 ? -1 : dist[n];//为什么是0x3f3f3f3f/2?若a的距离为无穷,用0x3f3f3f3f表示, 若a-b之间权重为-w, dist[b] = 0x3f3f3f3f-w(w<10000) } public static void main(String[] args){ Scanner sc = new Scanner(new BufferedInputStream(System.in)); n = sc.nextInt(); m = sc.nextInt(); k = sc.nextInt(); for(int i = 0; i < m; i++){ int x = sc.nextInt(), y = sc.nextInt(), z = sc.nextInt(); edge[i] = new Edge(x, y, z); //创建一个边类,包含属性a, b, w } int t = bellman_ford(); if(t == -1)System.out.println("impossible"); else System.out.println(t); } }
spfa算法 1.适用情况:存在负权边,可以用来求最短路以及判断图中是否存在负环。
2.时间复杂度:一般为$O(m)$, 最坏为$O(nm)$.
3.图存储方式:邻接表
4.算法描述:spfa算法是对bellman-ford算法的改进,bellman-ford算法每一次会对所有边都进行松弛操作($dist[b] = min(dist[b], dist[a] + w)$),但是不一定每次$dist[b]$都会改变,若$dist[b]$要变,只有当$dist[a]$变小。spfa对这一步进行优化,不用每一次都进行松弛操作,维护一个队列,队列里的元素是距离被更新的点(若$dist[b]$变小,就把$b$放到队列),用这些距离被更新的点去更新与这个点相邻的其他点。
5.代码实现:
求1号点到n号点的最短路:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public static int spfa(){ Arrays.fill(dist, 0x3f3f3f3f); dist[1] = 0; queue.offer(1); //源点放入队列 st[1] = true; //st标志队列里面的元素,防止队列出现重复元素 while(!queue.isEmpty()){ int t = queue.poll(); st[t] = false; for(int i = h[t]; i != -1; i = ne[i]){//遍历邻接表 int j = e[i]; if(dist[j] > dist[t] + w[i]){ dist[j] = dist[t] + w[i]; if(!st[j]){//若j的距离变小,并且j不在队列中,则将j放到队列中 queue.offer(j); st[j] = true; } } } } return dist[n] == 0x3f3f3f3f ? -1 : dist[n]; }
判断图中是否存在负环:int[] cnt用来记录存1-x最短距离的边数,当$cnt[x] >= n$时,则可以判断图中存在负环(因为$cnt[x] >= n$意味着1-x有$n$条边,$n+1$个点,但是图中只有$n$个点,所以有两个点必然重合)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public static boolean spfa(){ Arrays.fill(dist, 0x3f3f3f3f); dist[1] = 0; // 由于是判断图中是否存在负环,不是从1开始的负环,所以把每个点都加入队列, for(int i = 1; i <= n; i++){ queue.offer(i); st[i] = true; } while(!queue.isEmpty()){ int t = queue.poll(); st[t] = false; for(int i = h[t]; i != -1; i = ne[i]){ int j = e[i]; if(dist[j] > dist[t] + w[i]){ dist[j] = dist[t] + w[i]; cnt[j] = cnt[t] + 1; //更新边数 if(cnt[j] >= n)return true; if(!st[j]){ queue.offer(j); st[j] = true; } } } } return false; }
多源最短路 floyd算法 1.适用情况:求任意两个点之间的最短路径。
2.时间复杂度:$O(n^3)$
3.图存储方式:邻接矩阵
4.算法描述:初始时,$d$为邻接矩阵,经过$d[i, j] = min(d[i, j],d[i, k] + d[k, j])$的$n^3$次计算,最后$d[i, j]$表示$i$到$j$的最短路径长度。
5.代码实现:
1 2 3 4 5 6 7 8 9 public static void floyd(){ for(int k = 1; k <= n; k++){ for(int i = 1; i <= n; i++){ for(int j = 1; j <= n; j++){ d[i][j] = Math.min(d[i][j], d[i][k] + d[k][j]); } } } }