图论中的最短路问题
前置知识:边( u , v ) (u,v) ( u , v ) 的松弛操作:d i s ( v ) = m i n ( d i s ( v ) , d i s ( u ) + w ) dis(v)=min(dis(v),dis(u)+w) d i s ( v ) = m i n ( d i s ( v ) , d i s ( u ) + w )
一般实际中写if((ll)d[v]>(ll)d[u]+w)
,防止赋了0x7fffffff
的d
数组加法溢出;另一种方法是赋值0x3f3f3f3f
单源最短路径
即求解给定的单个出发点到图中其他点的最短路径
Bellman-Ford
可以负权,可以判断是否有负环,O ( n m ) O(nm) O ( n m ) 。
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 bool bellmanford (int n, int s) { d[s] = 0 ; bool flag; for (int i = 1 ; i <= n; i++) { flag = false ; for (int u = 1 ; u <= n; u++) { for (auto it : G[u]) { int v = it.to, w = it.val; if (d[v] > d[u] + w) { d[v] = d[u] + w; flag = true ; } } } if (!flag) break ; } return flag; }
SPFA
队列优化的Bellman-Ford,复杂度玄学
原理:松弛操作只可能在之前有过松弛操作的边端点产生
关于SPFA,它死了
出自某年NOI,因为造适当的数据可以将SPFA卡成最坏情况O ( n m ) O(nm) O ( n m )
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 queue<int > q; bool vst[N];int cnt[N];bool spfa () { d[s] = 0 , vst[s] = true ; q.push (s); while (!q.empty ()) { int u = q.front (); q.pop (); vst[u] = false ; for (auto it : G[u]) { int v = it.to, w = it.val; if (d[v] > d[u] + w) { d[v] = d[u] + w; cnt[v] = cnt[u] + 1 ; if (cnt[v] >= n) return false ; if (!vst[v]) q.push (v), vst[v] = true ; } } } return true ; }
Dijkstra
没有负权边,时间复杂度O ( m log m ) O(m\log m) O ( m log m )
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 typedef pair<int , int > P; int n, m, cnt, s; int d[N]; bool vst[N];void Dijkstra () { priority_queue<P, vector<P>, greater<P>> dij; dij.push (P (0 , s)); d[s] = 0 ; while (!dij.empty ()) { int cur = dij.top ().second; dij.pop (); if (vst[cur]) continue ; vst[cur] = true ; for (int i = head[cur]; ~i; i = edge[i].nxt) { int v = edge[i].to, w = edge[i].val; if (d[v] > d[cur] + w) { d[v] = d[cur] + w; dij.push (P (d[v], v)); } } } }
多源最短路径
Floyd
无负环图,多源最短路,时间复杂度O ( n 3 ) O(n^3) O ( n 3 )
1 2 3 4 5 6 7 8 9 for (int k = 1 ; k <= n; k++) for (int i = 1 ; i <= n; i++) { if (d[i][k] == INF || i == k) continue ; for (int j = 1 ; j <= n; j++) d[i][j] = min ((ll)d[i][j], (ll)d[i][k] + d[k][j]); }
正权无向图求最小权值环:跑一遍找d [ i ] [ i ] d[i][i] d [ i ] [ i ] 最小的,O ( n 3 ) O(n^3) O ( n 3 ) 。
判断是否有负环:去掉i = = k i==k i = = k ,跑完检查d [ i ] [ i ] d[i] [i] d [ i ] [ i ] 是否为负,若有为负的就有负环
有向图判断任意两点是否连通:跑一遍,把m i n min m i n 改成或运算d[i][j]|=d[i][k]&d[k][j]
,还可以用bitset 优化成O ( n 3 w ) O(\frac{n^3}{w}) O ( w n 3 ) :
1 2 3 4 for (int k = 1 ; k <= n; k++) for (int i = 1 ; i <= n; i++) if (d[i][k]) d[i] = d[i] | d[k];
如果需要记录路径,记录的是p r e [ i ] [ j ] = k pre[i][j]=k p r e [ i ] [ j ] = k 。
Johnson
无负环图,复杂度O ( n m log m ) O(nm\log m) O ( n m log m ) 。
核心思想:修改路径权值使其能跑dijkstra
步骤:
新建0号节点,向每个点连一条长度为0的边
跑一遍0号起点开始的Bellman-Ford,得到0号点到每个点的最短路d i s [ v ] dis[v] d i s [ v ]
将边( u , v ) (u,v) ( u , v ) 的边权修改为w + d i s [ u ] − d i s [ v ] w+dis[u]-dis[v] w + d i s [ u ] − d i s [ v ]
跑n轮dijkstra
正确性:
将修改后的边权,带入某条路径长的计算,结果为∑ w i + d i s [ s ] − d i s [ t ] \sum w_i +dis[s]-dis[t] ∑ w i + d i s [ s ] − d i s [ t ] ,s s s 是起点,t t t 是终点,因此最短路的性质没有改变。
BellmanFord对所有边进行了松弛操作d i s [ u ] + w > = d i s [ v ] dis[u]+w>=dis[v] d i s [ u ] + w > = d i s [ v ] ,保证修改后边权的非负性。
注意:改spfa时判断轮数是n+1(添加了一个节点)
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 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 struct Edge { int to, val, nxt; } edge[M]; int head[N]; int tot;void add_edge (int u, int v, int w) { ++tot; edge[tot].to = v; edge[tot].val = w; edge[tot].nxt = head[u]; head[u] = tot; } int dis[N];int d[N][N];int n, m;bool vst[N];int cnt[N];bool spfa () { queue<int > q; dis[0 ] = 0 , vst[0 ] = true ; q.push (0 ); while (!q.empty ()) { int u = q.front (); q.pop (), vst[u] = false ; for (int i = head[u]; ~i; i = edge[i].nxt) { int v = edge[i].to, w = edge[i].val; if (dis[v] > dis[u] + w) { dis[v] = dis[u] + w; if (!vst[v]) { cnt[v]++; if (cnt[v] > n) return true ; q.push (v); vst[v] = true ; } } } } return false ; } priority_queue<pii, vector<pii>, greater<pii>> q; void dijkstra (int d[], int s) { while (!q.empty ()) q.pop (); for (int i = 1 ; i <= n; i++) vst[i] = false ; d[s] = 0 ; q.push (make_pair (0 , s)); while (!q.empty ()) { int cur = q.top ().second; q.pop (); if (vst[cur]) continue ; vst[cur] = true ; for (int i = head[cur]; ~i; i = edge[i].nxt) { int v = edge[i].to, w = edge[i].val; if (d[v] > (ll)d[cur] + w) { d[v] = d[cur] + w; q.push (make_pair (d[v], v)); } } } } int main () { read (n), read (m); for (int i = 0 ; i <= n; i++) head[i] = -1 ; int u, v, w; for (int i = 1 ; i <= m; i++) { read (u), read (v), read (w); add_edge (u, v, w); } for (int i = 1 ; i <= n; i++) { add_edge (0 , i, 0 ); dis[i] = INF; } if (spfa ()) { puts ("-1" ); return 0 ; } for (int i = 1 ; i <= n; i++) { for (int j = head[i]; ~j; j = edge[j].nxt) edge[j].val += dis[i] - dis[edge[j].to]; for (int j = 1 ; j <= n; j++) d[i][j] = INF; } for (int i = 1 ; i <= n; i++) dijkstra (d[i], i); for (int i = 1 ; i <= n; i++) { ll ans = 0 ; for (int j = 1 ; j <= n; j++) ans += (ll)j * (d[i][j] == INF ? INF : (d[i][j] - dis[i] + dis[j])); printf ("%lld\n" , ans); } return 0 ; }
分层图
图论题一般二维,i i i 点到j j j 点,有些题对这个过程有多种选择,引入第三维,将其“分层”,和dp中增加维数一个道理,在松弛操作中维护。
题目
P4568 JLOI2011 飞行路线 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
P1266 速度限制 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
k短路
建正反图,跑反图的最短路,再调整边取前k
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 bool vst[N];double dis[N];struct node { int x; double v; bool operator <(const node &rhs) const { return v + dis[x] > rhs.v + dis[rhs.x]; } }; priority_queue<pii> Q; void dijkstra () { for (int i = 1 ; i <= n; i++) dis[i] = INF; dis[ed] = 0 ; Q.push (make_pair (0 , ed)); while (!Q.empty ()) { int cur = Q.top ().second; Q.pop (); if (vst[cur]) continue ; vst[cur] = true ; for (int i = head[cur]; i; i = edge[i].nxt) { int v = edge[i].to; double w = edge[i].val; if (dis[v] > dis[cur] + w) { dis[v] = dis[cur] + w; Q.push (make_pair (dis[v], v)); } } } } int cnt[N];priority_queue<node> q; dijkstra ();q.push ({st, 0 }); while (!q.empty ()){ int cur=q.top ().x, w=q.top ().v; q.pop (); cnt[cur]++; if (cur == ed && cnt[cur] == k) { printf ("%.2lf\n" , w); return 0 ; } if (cnt[cur] > k) continue ; for (int i = head[cur]; i; i = edge[i].nxt) q.push ({edge[i].to, edge[i].val + w}); } printf ("-1\n" );
最短路计数
在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 ... int cnt[N];void Dijkstra () { ... cnt[s] = 1 ; while (!dij.empty ()) { ... for (int i = head[cur]; ~i; i = edge[i].nxt) { int v = edge[i].to, w = edge[i].val; if (d[v] > d[cur] + w) { d[v] = d[cur] + w; dij.push (P (d[v], v)); cnt[v] = cnt[cur]; } else if (d[v] == d[cur] + w) cnt[v] += cnt[cur]; } } }
基于此可以延伸一些诸如次短路计数的鬼东西,可以把d
和cnt
都设置成二维,一个记最短路一个记次短路,分类讨论更新就行了。