最短路
难点在建图,如何把问题抽象成一个图,转化成最短路问题。
单源最短路
所有边权都是正数
朴素 Dijkstra 算法
n 为节点数。
时间复杂度:O ( n 2 ) O(n^{2}) O ( n 2 )
适合稠密图。即 m 与 n 2 n^{2} n 2 是一个级别的,图中边比较多。
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 #include <iostream> #include <cstring> using namespace std;const int N = 510 ;int g[N][N];int d[N];bool st[N];int n, m;int dijkstra () { d[1 ] = 0 ; for (int i=0 ;i<n;i++) { int t = -1 ; for (int j=1 ;j<=n;j++) { if (!st[j] && (t == -1 || d[j] < d[t])) { t = j; } } st[t] = true ; for (int j=1 ; j<=n; j++) { d[j] = min (d[j], d[t] + g[t][j]); } } if (d[n] == 0x3f3f3f3f ) return -1 ; return d[n]; } int main () { memset (g, 0x3f , sizeof g); memset (d, 0x3f , sizeof d); scanf ("%d%d" , &n, &m); while (m--) { int a, b, c; scanf ("%d%d%d" , &a, &b, &c); g[a][b] = min (g[a][b], c); } int t = dijkstra (); printf ("%d\n" , t); return 0 ; }
堆优化 Dijkstra 算法
n 为节点数,m 为边数。
时间复杂度:O ( m ⋅ l o g ( n ) ) O(m \cdot log(n)) O ( m ⋅ l o g ( n ) )
适合稀疏图。即 m 与 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 #include <iostream> #include <cstring> #include <algorithm> #include <queue> #include <vector> using namespace std;const int N = 1e6 + 10 ;typedef pair<int , int > PII;int h[N], e[N], w[N], ne[N], idx;int n, m;bool st[N];int d[N];void add (int a, int b, int c) { e[idx] = b; w[idx] = c; ne[idx] = h[a]; h[a] = idx++; } int dijkstra () { d[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, dist = 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 + w[i] < d[j]) { d[j] = dist + w[i]; heap.push ({d[j], j}); } } } if (d[n] == 0x3f3f3f3f ) return -1 ; return d[n]; } int main () { memset (h, -1 , sizeof h); memset (d, 0x3f , sizeof h); scanf ("%d%d" , &n, &m); while (m--) { int a, b, c; scanf ("%d%d%d" , &a, &b, &c); add (a, b, c); } int t = dijkstra (); printf ("%d\n" , t); return 0 ; }
存在负权边
Bellman-Ford 算法
时间复杂度:O ( n ⋅ m ) O(n \cdot m) O ( n ⋅ m )
可以求不超过 k 条边的最短路。
基本框架
1 2 3 4 for n 次 - 迭代 k 次,就是求不超过 k 条边的最短路 for 所有边 <a, b, w> // 看一下从节点 1 走到节点 a 再从节点 a 走到 b 是否比直接从节点 1 走到节点 b 的路径要短,如果短就更新 dist[b] 的值 dist[b] = min(dist[b], dist[a] + w); // 松弛操作
运行完后可以保证:dist[b] <= dist[a] + w
,叫做三角不等式 。
有边数限制的最短路
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 #include <iostream> #include <cstring> using namespace std;int n, m, k;const int M = 10010 , N = 510 ;int d[N], backup[N];struct Edge { int a, b, w; }edges[M]; int bellman_ford () { memset (d, 0x3f , sizeof d); d[1 ] = 0 ; for (int i=0 ; i<k; i++) { memcpy (backup, d, sizeof d); for (int j=0 ;j<m;j++) { int a = edges[j].a, b = edges[j].b, w = edges[j].w; d[b] = min (d[b], backup[a] + w); } } if (d[n] > 0x3f3f3f3f / 2 ) return -1 ; return d[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 算法(Shortest Path Fast Algorithm)
对 Bellman-Ford 算法进行了优化。
不能有负权回路 ,否则会死循环 。
时间复杂度一般情况下:O ( m ) O(m) O ( m )
时间复杂度最坏情况下:O ( n ⋅ m ) O(n \cdot m) O ( n ⋅ m )
spfa 求最短路
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 #include <iostream> #include <queue> #include <cstring> using namespace std;const int N = 1e5 + 10 ;int h[N], e[N], w[N], ne[N], idx;int n, m;bool st[N];int d[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 (d, 0x3f , sizeof d); d[1 ] = 0 ; queue<int > q; q.push (1 ); st[1 ] = true ; while (q.size ()) { auto t = q.front (); q.pop (); st[t] = false ; for (int i=h[t];i!=-1 ;i=ne[i]) { int j = e[i]; if (d[j] > d[t] + w[i]) { d[j] = d[t] + w[i]; if (!st[j]) { q.push (j); st[j] = true ; } } } } if (d[n] == 0x3f3f3f3f ) return -1 ; return d[n]; } int main () { memset (h, -1 , sizeof h); cin >> n >> m; while (m--) { int a, b, w; cin >> a >> b >> w; add (a, b, w); } int t = spfa (); if (t == -1 ) cout << "impossible" << endl; else cout << t << endl; return 0 ; }
Tips:
Bellman_ford 算法里最后 return -1
的判断条件写的是 dist[n] > 0x3f3f3f3f / 2
;而 SPFA 算法写的是 dist[n] == 0x3f3f3f3f
。其原因在于 Bellman_ford 算法会遍历所有的边,因此不管是不是和源点连通的边它都会得到更新;但是 SPFA 算法不一样,它相当于采用了 BFS,因此遍历到的结点都是与源点连通的,因此如果你要求的 n 和源点不连通,它不会得到更新,还是保持 0x3f3f3f3f
。
求负环一般使用 SPFA 算法,使用一个 cnt 数组记录每个节点到源点经过的边数,每更新一次 d[j]
,cnt[j]
就加一。一旦某个 cnt[x]
到达 n,就说明存在负环。
多源汇最短路
源 指的就是起点 。
汇 指的就是终点 。
Floyd 算法
时间复杂度:O ( n 3 ) O(n^{3}) O ( n 3 )
图不能有负权回路。
直接将邻接矩阵 转换为最短路径矩阵 。d[a][b]
表示从 a 节点到 b 节点的最短距离。
Floyd 求最短路
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 #include <iostream> #include <algorithm> using namespace std;const int N = 200 , INF=1e9 ;int n,m,Q;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, &Q); 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 (Q--) { int a, b; scanf ("%d%d" , &a, &b); if (d[a][b] > INF / 2 ) puts ("impossible" ); else printf ("%d\n" , d[a][b]); } return 0 ; }