常见的最短路问题:
单源最短路 :求一个点到其他所有点的最短距离,如从1号点到N号点的最短路。又可分为:
多源汇最短路 :可能有多个询问,每次询问从其中一个点走到另一点的最短距离,即起点和终点不确定(源点——起点,汇点——终点):
最短路问题考察的重点难点是:建图,即把原问题抽象成一个最短路问题,如何定义点和边 ;重点不在证明算法的正确性上。
(有向图和无向图的最短路问题其实是没有区别的,无向图可以看作是特殊的有向图,我们可以用有向图的最短路算法来解决无向图的问题。)
朴素Dijkstra 1 2 3 4 5 6 7 8 9 初始化距离 dis[1 ] = 0 , dis[i] = 正无穷 for i 从0 到n: (集合S存当前已经确定最短距离的点){ t = 找到不在S中的距离最近的点 t加到s中 用t更新其他点的距离(从t出去的所有边,它组成的路径能不能更新其他点的距离,即dis[x] > dis[t] + w) } 每次循环都可以确定一个点的最短距离,循环n次后就得到了每个点到起点的最短距离
例题:Dijkstra求最短路I(Acwing 849)
给定一个n个点m条边的有向图,图中可能存在重边和自环,所有边权均为正值。请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出-1。
输入格式:第一行包含整数n和m。接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
输出格式:输出一个整数,表示1号点到n号点的最短距离。如果路径不存在,则输出-1。
数据范围:$1 \le n \le500, 1 \le m \le 105$,图中涉及边长均不超过10000。
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 46 47 48 49 50 51 52 #include <cstring> #include <iostream> #include <algorithm> using namespace std ;const int N = 510 ;int n, m;int g[N][N];int dist[N]; bool st[N]; int dijkstra () { memset (dist, 0x3f , sizeof dist); dist[1 ] = 0 ; for (int i = 0 ; i < n - 1 ; i++) { int t = -1 ; for (int j = 1 ; j <= n; j++) if (!st[j] && (t == -1 || dist[t] > dist[j])) t = j; for (int j = 1 ; j <= n; j++) dist[j] = min (dist[j], dist[t] + g[t][j]); st[t] = true ; } if (dist[n] == 0x3f3f3f3f ) return -1 ; return dist[n]; } int main () { scanf ("%d%d" , &n, &m); memset (g, 0x3f , sizeof g); while (m--) { int a, b, c; scanf ("%d%d%d" , &a, &b, &c); g[a][b] = min (g[a][b], c); } printf ("%d\n" , dijkstra()); return 0 ; }
堆优化Dijkstra 回顾一下朴素Dijkstra算法中的各步骤的时间复杂度:
1 2 3 4 5 6 for i 从0 到n: (集合S存当前已经确定最短距离的点){ t = 找到不在S中的距离最近的点————总共n^2 次 t加到s中————总共n次 用t更新其他点的距离(从t出去的所有边,它组成的路径能不能更新其他点的距离,即dis[x] > dis[t] + w)————总共m次(与边数有关) }
可见朴素Dijkstra算法中最耗时的是第一步:在st[]=false
的点中找到dist[]
最小的点,即是t,整个算法的时间复杂度为$O(n^2)$。在一组数中找到最小的数,正是堆擅长做的,在堆中求最小值的时间复杂度为$O(1)$,但这也会影响第三步,在堆中修改一个数的时间复杂度为$O(\log n)$。因此堆优化的Dijkstra算法中各步骤的时间复杂度为:
1 2 3 4 5 6 for i 从0 到n: (集合S存当前已经确定最短距离的点){ t = 找到不在S中的距离最近的点————总共n次 t加到s中————总共n次 用t更新其他点的距离(从t出去的所有边,它组成的路径能不能更新其他点的距离,即dis[x] > dis[t] + w)————总共 m*logn次 }
可见堆优化Dijkstra算法中最耗时的变成了第三步,整个算法的时间复杂度为$O(m \log n)$。
实现堆有两种方法:手写堆(可以支持修改任意一个元素);优先级队列(不支持修改任意一个元素,每修改一次就在队列中加一个新的数)。一般来说堆优化版的Dijkstra算法就使用STL中的优先级队列就行了。同时也要注意,稀疏图的存储方式用邻接表要好一些。
例题:Dijkstra求最短路II(Acwing 850)
给定一个n个点m条边的有向图,图中可能存在重边和自环,所有边权均为非负值。请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出-1。
输入格式:第一行包含整数n和m。接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
输出格式:输出一个整数,表示1号点到n号点的最短距离。如果路径不存在,则输出-1。
数据范围:$1 \le n, m \le 1.5 \times 10^5$,图中涉及边长均不小于0,且不超过10000。
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 #include <cstring> #include <iostream> #include <algorithm> #include <queue> using namespace std ;const int N = 1000010 ;typedef pair<int , int > PII; int n, m;int h[N], w[N], e[N], ne[N], idx; int dist[N]; bool st[N]; void add (int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++; } int dijkstra () { memset (dist, 0x3f , sizeof dist); dist[1 ] = 0 ; priority_queue<PII, vector <PII>, greater<PII>> heap; heap.push({0 , 1 }); while (heap.size ()) { auto t = heap.top(); heap.pop(); int ver = t.second, distance = t.first; if (st[ver]) continue ; st[ver] = true ; for (int i = h[ver]; i != -1 ; i = ne[i]) { int j = e[i]; if (dist[j] > distance + w[i]) { dist[j] = distance + w[i]; heap.push({dist[j], j}); } } } if (dist[n] == 0x3f3f3f3f ) return -1 ; return dist[n]; } int main () { scanf ("%d%d" , &n, &m); memset (h, -1 , sizeof h); while (m--) { int a, b, c; scanf ("%d%d%d" , &a, &b, &c); add(a, b, c); } printf ("%d\n" , dijkstra()); return 0 ; }
Bellman-Ford Bellman-Ford算法是用来解决存在权重是负数的边的最短路问题的,它的主要步骤为:
1 2 3 for n 次: for 所有边a, b, w: (a指向b,权重/边长为w) dist[b] = min (dist[b], dist[a] + w); (和Dijkstra类型,dist[]存点到起点的最短距离)
Bellman-Ford遍历完所有边后,所有的点都满足:dist[b] <= dist[a] + w
(三角不等式),这个更新的过程叫作:松弛操作。
要注意的是:若存在负权回路,则最短路不一定是存在的 (若负环是在从起点到终点的某一条路径中,绕负权回路无数圈,最短路就是负无穷 了)。Bellman-Ford算法可以求出是否存在负权回路 ,其迭代的次数是有实际意义的,如当前迭代了k次,dist[]
数组的含义是从起点经过不超过K条边到各个点的最短距离;迭代完n次,第n次还有更新的话 就表示存在一条最短路径,其含有的边数为n条。如果一条最短路径上有n条边,但只有n-1个点,根据抽屉原理,路径上一定存在环,且又因为第n次更新了,所有环的权重一定是负的,即存在负权回路 。
例题:有边数限制的最短路(Acwing 853)
给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数。请你求出从1号点到n号点的最多经过k条边的最短距离,如果无法从1号点走到n号点,输出impossible。注意:图中可能 存在负权回路 。
输入格式:第一行包含三个整数n,m,k。接下来m行,每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
输出格式:输出一个整数,表示从1号点到n号点的最多经过k条边的最短距离。如果不存在满足条件的路径,则输出“impossible”。
数据范围:$1 \le n,k \le 500,1 \le m \le 10000$,任意边长的绝对值不超过10000。
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 46 47 48 49 50 51 52 53 54 #include <iostream> #include <cstring> #include <algorithm> using namespace std ;const int N = 510 , M = 10010 ;int n, m, k;int dist[N], backup[N];struct Edge { int a, b, w; }edges[M]; int bellman_ford () { memset (dist, 0x3f , sizeof dist); dist[1 ] = 0 ; for (int i = 0 ; i < k; i++) { memcpy (backup, dist, sizeof dist); for (int j = 0 ; j < m; j++) { int a = edges[j].a, b = edges[j].b, w = edges[j].w; dist[b] = min (dist[b], backup[a] + w); } } if (dist[n] > 0x3f3f3f3f / 2 ) return -1 ; return dist[n]; } int main () { scanf ("%d%d%d" , &n, &m, &k); for (int i = 0 ; i <m; i++) { int a, b, w; scanf ("%d%d%d" , &a, &b, &w); edges[i] = {a, b, w}; } int t = bellman_ford(); if (t == -1 ) puts ("impossible" ); else printf ("%d\n" , t); return 0 ; }
SPFA SPFA算法也是用来解决存在权重是负数的边的最短路问题的,它要求路径中没有负环(负权回路),其实大多数最短路题目中都是没有负环的,SPFA算法也算是单源最短路问题中限制最少的算法了。SPFA算法相当于是在Bellman Ford算法的基础上做优化,回顾下Bellman Ford算法:
1 2 3 for n 次: for 所有边a, b, w: (a指向b,权重/边长为w) dist[b] = min (dist[b], dist[a] + w); (和Dijkstra类型,dist[]存点到起点的最短距离)
每次迭代要更新dist[b]
时如果dist[b]
在当前迭代中想要变小,那就要求dist[a]
要变小,只要dist[a]
变小了,dist[b]
才会变小。SPFA就是从这一点作优化,利用宽度优先搜索BFS来做优化,其思路如下:
1 2 3 4 5 6 7 8 起点放入队列 queue while queue 不空: (queue存的是所有dist[]变小了的节点){ 取出队头 t = q.fornt(); q.pop(); 更新t的所有出边 t-b, 权重为w 若更新成功,且队列中没有b,就把b加入队列 }
例题:SPFA求最短路(Acwing 851)
给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数。请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出impossible。数据保证不存在负权回路。
输入格式:第一行包含整数n和m。接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
输出格式:输出一个整数,表示1号点到n号点的最短距离。如果路径不存在,则输出”impossible”。
数据范围:$1 \le n,m \le 10^5$,图中涉及边长绝对值均不超过10000。
(代码与Dijkstra算法的很像)
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 #include <cstring> #include <iostream> #include <algorithm> #include <queue> using namespace std ;const int N = 100010 ;int n, m;int h[N], w[N], e[N], ne[N], idx; int dist[N]; bool st[N]; void add (int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++; } int spfa () { memset (dist, 0x3f , sizeof dist); dist[1 ] = 0 ; queue <int > q; q.push(1 ); st[1 ] = true ; while (q.size ()) { int t = q.front(); q.pop(); 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]) { q.push(j); st[j] = true ; } } } } if (dist[n] == 0x3f3f3f3f ) return -1 ; return dist[n]; } int main () { scanf ("%d%d" , &n, &m); memset (h, -1 , sizeof h); while (m--) { int a, b, c; scanf ("%d%d%d" , &a, &b, &c); add(a, b, c); } int t = spfa(); if (t == -1 ) puts ("impossible" ); else printf ("%d\n" , t); return 0 ; }
SPFA算法也可以用来判断路径中是否存在负环 (判断负环一般用SPFA算法,时间复杂度低)。
例题2:SPFA判断负环(Acwing 852)
给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数。请你判断图中是否存在负权回路。
输入格式:第一行包含整数n和m。接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
输出格式:如果图中存在负权回路,则输出“Yes”,否则输出“No”。
数据范围:$1 \le n \le 2000,1 \le m \le 10000$,图中涉及边长绝对值均不超过10000。
用dist[x]
数组表示从起点到当前点的最短距离,cnt[x]
数组表示当前的最短路中边的数量,每次更新时有:
1 2 dist[x] = dist[x] + w[i]; cnt[x] = cnt[t] + 1 ;
如果过程中每一更新后cnt[x] >= 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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 #include <cstring> #include <iostream> #include <algorithm> #include <queue> using namespace std ;const int N = 10010 ;int n, m;int h[N], w[N], e[N], ne[N], idx; int dist[N], cnt[N]; bool st[N]; void add (int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++; } bool spfa () { queue <int > q; for (int i = 1 ; i <= n; i++) { st[i] = true ; q.push(i); } while (q.size ()) { int t = q.front(); q.pop(); 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]) { q.push(j); st[j] = true ; } } } } return false ; } int main () { scanf ("%d%d" , &n, &m); memset (h, -1 , sizeof h); while (m--) { int a, b, c; scanf ("%d%d%d" , &a, &b, &c); add(a, b, c); } int t = spfa(); if (spfa()) puts ("Yes" ); else puts ("No" ); return 0 ; }
Floyd Floyd算法是用来解决多源汇最短路问题的 ,时间复杂度为$O(n^3)$,其思路是:
1 2 3 4 5 6 7 用一个邻接矩阵d[i][j]存储图中所有的边; for (k = 1 ; k <= n; k++) for (i = 1 ; i <= n; i++) for (j = 1 ; j <= n; j++) d[i][j] = min (d[i][j], d[i][k] + d[k][j]); 循环结束之后,d[i][j]存的是从点i到点j的最短路的长度
Floyd算法是基于动态规划,$d[k, i, j]$表示从点$i$到点$j$,只经过1到$k$这些中间点,的最短距离,更新$d[k, i, j]$时有:
(关于动态规划的知识,之后会细讲。)
例题:Floyd算法求最短路(Acwing 854)
给定一个n个点m条边的有向图,图中可能存在重边和自环,边权可能为负数。再给定k个询问,每个询问包含两个整数x和y,表示查询从点x到点y的最短距离,如果路径不存在,则输出“impossible”。数据保证图中不存在负权回路。
输入格式:第一行包含三个整数n,m,k。接下来m行,每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。接下来k行,每行包含两个整数x,y,表示询问点x到点y的最短距离。
输出格式:共k行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出“impossible”。
数据范围:$1 \le n \le 200, 1 \le k \le n^2, 1 \le m \le 20000$,图中涉及边长绝对值均不超过10000。
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 46 47 #include <iostream> #include <cstring> #include <algorithm> using namespace std ;const int N = 210 , INF = 1e9 ;int n, m, k;int d[N][N]; 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] = min (d[i][j], d[i][k] + d[k][j]); } int main () { scanf ("%d%d%d" , &n, &m, &k); for (int i = 1 ; i <=n; i++) for (int j = 1 ; j <= n; j++) if (i == j) d[i][j] = 0 ; else d[i][j] = INF; while (m--) { int a, b, w; scanf ("%d%d%d" , &a, &b, &w); d[a][b] = min (d[a][b], w); } floyd(); while (k--) { int a, b; scanf ("%d%d" , &a, &b); if (d[a][b] > INF / 2 ) puts ("impossible" ); else printf ("%d\n" , d[a][b]); } }