最短路
单源最短路
所有边权都是正数
朴素 Dijkstra算法 O(n^2) 遍历所有点
Dijkstra堆优化算法 O(mlogn) 遍历所有最短点并更新相连的所有边
存在负权边
Bellman-Ford O(nm) 遍历所有边, 更新点. 可求限定边的最短路问题
SPFA 一般O(m) 最坏O(mn)
多源汇最短路
源点 : 起点
汇点 : 重点
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 int nint 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]; }
堆优化版本 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 typedef pair<int , int > PII;const int N = 1e6 + 10 ;int n, m;int h[N], w[N], e[N], ne[N], idx; int dist[N]; bool st[N]; 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] > dist[ver] + w[i]) { dist[j] = dist[ver] + w[i]; heap.push ({dist[j], j}); } } } if (dist[n] == 0x3f3f3f3f ) return -1 ; return dist[n]; }
Bellman - Ford 算法
核心操作是遍历所有的边, 刷新点的距离
对于每一次松弛操作, 我们都要备份一次操作过的数组防止覆盖掉当前操作
a, b ,w a指向b的边w
1 2 3 4 5 6 7 循环n次 // 不超过k条边最短路的距离 { for 所有边w // 松弛操作 { dist[b] = min (dist[b] ,dist[a] + w) } }
算法执行结束后, 对于所有的边 dist[b] <= dist[a] + w 三角不等式
一定要备份数组, 不然会出现修改原dist数组的情况,导致结果混乱
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void bellman_ford () { memset (dist, 0x3f , sizeof dist); dist[1 ] = 0 ; for (int i = 0 ; i < k; i ++ ) { for (int j = 0 ; j < m; j ++ ) { auto e = edges[j]; dist[e.b] = min (dist[e.b], dist[e.a] + e.c); } } }
代码模板
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 #include <iostream> #include <cstring> #include <algorithm> using namespace std;const int N = 10010 ;struct Edge { int a; int b; int c; }w[N]; int n, m, k;int dist[N];int last[N];int main () { cin >> n >> m >> k; for (int i = 0 ; i < m; i ++) { cin >> w[i].a >> w[i].b >> w[i].c; } memset (dist, 0x3f , sizeof dist); dist[1 ] = 0 ; for (int j = 0 ; j < k; j++) { memcpy (last, dist, sizeof dist); for (int i = 0 ; i < m ; i++) { int a = w[i].a, b = w[i].b, c = w[i].c; dist[b] = min (dist[b], last[a] + c); } } if (dist[n] > 0x3f3f3f3f / 2 ) cout << "impossible" ; else cout << dist[n]; return 0 ; }
spfa算法 (可判断负权环) 使用一个队列, 将更新后缩小的路径添加至队列, st数组表示某一点是否在队列中, 因为某一天有可能在遍历多个边之后, 路径缩短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 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 ; } } } } return dist[n]; }
判断负权回路的方法 我们使用一个cnt数组记录当前走过的路径, 如果cnt > 经过的点, 则证明当前图中存在负权回路
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 int cnt[N]; 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 ; }
为什么不需要初始化dist数组? 如果只是为了判断负环, 如果负环存在的话dist的值会变为无穷小,所以dist初始值无论是多少都不会影响判断负环
多源汇最短路Floyd 解决多个源点到汇点的距离
1 2 3 4 5 6 7 8 9 10 11 12 13 for k for i for j d[i][j] = min ( d[i][j], d[i][k] + d[k][j]) 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]); }
最小生成树
prim算法求最小生成树 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 int prim (int n) { int ret = 0 ; memset (dist, 0x3f , sizeof dist); for (int i = 0 ; i < n; i++) { int t = -1 ; for (int j = 1 ; j <= n; j++) { if (!st[j] && (t == -1 || dist[t] > dist[j])) t = j; } if (i && dist[t] == 0x3f3f3f3f ) return 0x3f3f3f3f ; if (i) ret += dist[t]; st[t] = 1 ; for (int j = 1 ; j <= n; j++) { if (dist[j] > e[t][j]) dist[j] = e[t][j]; } } return ret; }
克鲁斯卡尔算法 并查集的应用
对所有边按权重排序
从最小的开始依次查询两条边是否在一个集合内, 如果不在一个集合内将点加入集合并累加边的权重.
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 int kruskal () { sort (edges, edges + m); for (int i = 1 ; i <= n; i ++ ) p[i] = i; int res = 0 , cnt = 0 ; for (int i = 0 ; i < m; i ++ ) { int a = edges[i].a, b = edges[i].b, w = edges[i].w; a = find (a), b = find (b); if (a != b) { p[a] = b; res += w; cnt ++ ; } } if (cnt < n - 1 ) return INF; return res; }
二分图
染色法 O(m+n)
匈牙利算法 O(mn), 实际运行时间远小于O(mn)
染色法 深度优先遍历, 分别染色. 如果已经被染过则不是二分图
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 #include <iostream> #include <cstring> using namespace std;const int N = 100000 + 10 ;int h[N], e[2 * N], idx, ne[2 * N];int n, m;int st[N];void add (int a, int b) { e[idx] = b; ne[idx] = h[a]; h[a] = idx ++; } bool dfs (int x, int c) { st[x] = c; for (int i = h[x]; i != -1 ; i = ne[i]) { int d = e[i]; if (st[d] == 0 ) { if (!dfs (d,3 - c)) return false ; } if (st[d] == c) return false ; } return true ; } int main () { memset (h, -1 , sizeof h); cin >> n >> m; while (m--) { int a, b; cin >> a >> b; add (a,b); add (b,a); } bool ret = true ; for (int i = 1 ; i <= n; i++) { if (st[i] == 0 ) { if (!dfs (i,1 )) { ret = false ; break ; } } } if (ret) cout << "Yes" ; else cout << "No" ; return 0 ; }
匈牙利算法