单源最短路正权图朴素 DijkstraΘ ( n 2 ) \Theta(n^2) Θ ( n 2 ) 优化 DijkstraΘ ( m log m ) \Theta(m\log m) Θ ( m log m ) 负权图Bellman - FordΘ ( n m ) \Theta(nm) Θ ( n m ) SPFAΘ ( n m ) \Theta(nm) Θ ( n m ) 多源最短路负权图JohnsonΘ ( n m log m ) \Theta(nm\log m) Θ ( n m log m ) FloydΘ ( n 3 ) \Theta(n^3) Θ ( n 3 )
正权图的单源最短路Input Output 4 6 1 4 1 2 2 2 3 2 2 4 1 1 3 5 3 4 3 1 4 4 0 2 4 3 1 1 1 1 4<-2<-1
稠密图——朴素 Dijkstra从未确定最短路的点中选取最短路长度最小的点,为未确定最短路的点松弛出边,即d v = min { d v , d u + w u v } d_v=\min\left\lbrace d_v,\ d_u+w_{uv}\right\rbrace d v = min { d v , d u + w u v } ,循环n n n 轮。
时间复杂度Θ ( n 2 ) \Theta(n^2) Θ ( n 2 ) ,空间Θ ( n 2 ) \Theta(n^2) Θ ( n 2 ) ,适用于点数较少、边数较多的稠密图。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 auto Dijkstra = [&](int s) { vector<int > dis (n, inf), vis (n); dis[s] = 0 ; for (int t = 0 ; t < n; t++) { int u = 0 ; for (int i = 0 ; i < n; i++) { if (!vis[i] && dis[i] < dis[u]) u = i; } vis[u] = 1 ; for (int v = 0 ; v < n; v++) { if (!vis[v]) dis[v] = min (dis[v], dis[u] + E[u][v]); } } return dis; };
稀疏图——优化 Dijkstra从未确定最短路的点集Q Q Q 中选取最短路长度最小的点,松弛它的所有出边,并将这些点加入Q Q Q 。利用 priority queue 优化,存储{ d u , u } \left\lbrace d_u,\ u \right\rbrace { d u , u } ,队首元素即是「最短路长度最小的点」。
时间复杂度Θ ( m log m ) \Theta(m\log m) Θ ( m log m ) ,空间Θ ( n + m ) \Theta(n+m) Θ ( n + m ) ,适用于点数较少、边数较多的稠密图。
没有边权的 Dijkstra(01 BFS):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 auto bfs = [&](int s) { vector<int > dis (n, -1 ); dis[s] = 0 ; queue<int > Q; Q.push (s); while (!Q.empty ()) { int u = Q.front (); Q.pop (); for (auto & v : E[u]) { if (dis[v] != -1 ) continue ; dis[v] = dis[u] + 1 ; Q.push (v); } } return dis; };
一般的 Dijkstra:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 auto Dijkstra = [&](int s) { vector vis (n, 0 ); vector dis (n, inf) ; dis[s] = 0 ; priority_queue<pair<ll, int >, vector<pair<ll, int >>, greater<>> Q; Q.push ({ 0 , s }); while (!Q.empty ()) { auto [d, u] = Q.top (); Q.pop (); if (vis[u]) continue ; vis[u] = 1 ; for (auto [w, v] : E[u]) { if (dis[v] > w + d) { dis[v] = w + d; Q.push ({ dis[v], v }); } } } return dis; };
负权图的单源最短路——Bellman - Ford / SPFA与 Dijkstra 不同,每次循环为所有点松弛出边,而不是未确定最短路的点,循环n n n 轮。如果某轮中没有可以松弛的边,则最短路已经确定。如果n n n 轮后仍存在可以松弛的边,则存在负环。
时间复杂度Θ ( n m ) \Theta(nm) Θ ( n m ) 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 auto BellmanFord = [&](int s) { vector dis (n, inf); dis[s] = 0 ; for (int t = 0 ; t < n; t++) { bool flag = 0 ; for (int u = 0 ; u < n; u++) { for (auto [w, v] : E[u]) { if (dis[v] > dis[u] + w) { dis[v] = dis[u] + w; flag = 1 ; } } } if (!flag) return dis; } return vector <ll>(); };
vis 优化 BellmanFord:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 auto BellmanFord = [&](int s) { vector vis (n, 0 ); vector dis (n, inf) ; dis[s] = 0 ; for (int t = 0 ; t < n; t++) { bool flag = 0 ; for (int u = 0 ; u < n; u++) { if (vis[u]) continue ; for (auto [w, v] : E[u]) { if (dis[v] > dis[u] + w) { dis[v] = dis[u] + w; vis[v] = 0 ; flag = 1 ; } } vis[u] = 1 ; } if (!flag) return dis; } return vector <ll>(); };
Queue 优化 Bellman - Ford 的 SPFA,在随机图上可实现Θ ( n log n ) \Theta(n\log n) Θ ( n log n ) 的复杂度,但最坏时间复杂度仍是Θ ( n m ) \Theta(nm) Θ ( n m ) 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 auto SPFA = [&](int n, int s) { vector<int > vis (n), cnt (n); vector dis (n, inf) ; dis[s] = 0 , cnt[s] = 1 ; queue<int > Q; Q.push (s), vis[s] = 1 ; while (!Q.empty ()) { int u = Q.front (); Q.pop (); vis[u] = 0 ; for (auto & [w, v] : E[u]) { if (dis[v] > dis[u] + w) { dis[v] = dis[u] + w; if (!vis[v]) { Q.push (v), vis[v] = 1 ; if (++cnt[v] > n) return vector <ll>(); } } } } return dis; };
多源最短路Input Output 5 7 1 2 4 1 4 10 2 3 7 4 5 3 4 2 -2 3 4 -3 5 3 4 0 4 11 8 11 -1 0 7 4 7 -1 -5 0 -3 0 -1 -2 5 0 3 -1 -1 4 1 0 5 5 1 2 4 3 4 9 3 4 -3 4 5 3 5 3 -2 -1
稀疏图——Johnson因 Dijkstra 不能处理负权图,我们以虚拟的n + 1 n+1 n + 1 号节点为起始点跑负权图的单源最短路,为每个点引入点权d u d_u d u ,将边权更新为E u v : = E u v + d u − d v E_{uv} := E_{uv} + d_u - d_v E u v : = E u v + d u − d v ,使边权皆为正,再跑n n n 遍优化 Dijkstra。
时间复杂度Θ ( n m log m ) \Theta(nm\log m) Θ ( n 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 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 int n, m;cin >> n >> m; int N = n + 1 ;vector<vector<pii>> E (N); while (m--) { int u, v, w; cin >> u >> v >> w; u--, v--; E[u].push_back ({ w, v }); } int s = n; for (int i = 0 ; i < n; i++) { E[s].push_back ({ 0 , i }); } auto BellmanFord = [&](int s) { vector<bool > vis (N); vector dis (N, inf) ; dis[s] = 0 ; for (int t = 0 ; t < N; t++) { bool flag = 0 ; for (int u = 0 ; u < N; u++) { if (vis[u]) continue ; for (auto [w, v] : E[u]) { if (dis[v] > dis[u] + w) { dis[v] = dis[u] + w; vis[v] = 0 ; flag = 1 ; } } vis[u] = 1 ; } if (!flag) return dis; } return vector <ll>(); }; auto Dijkstra = [&](int s) { vector vis (N, 0 ); vector dis (N, inf) ; dis[s] = 0 ; priority_queue<pair<ll, int >, vector<pair<ll, int >>, greater<>> Q; Q.push ({ 0 , s }); while (!Q.empty ()) { auto [d, u] = Q.top (); Q.pop (); if (vis[u]) continue ; vis[u] = 1 ; for (auto [w, v] : E[u]) { if (dis[v] > w + d) { dis[v] = w + d; Q.push ({ dis[v], v }); } } } return dis; }; auto dis = BellmanFord (s);if (dis.empty ()) { cout << "-1\n\n" ; } else { for (int u = 0 ; u < n; u++) { for (auto & [w, v] : E[u]) w += dis[u] - dis[v]; } for (int u = 0 ; u < n; u++) { auto d = Dijkstra (u); for (int v = 0 ; v < n; v++) { if (d[v] == inf) cout << "-1 " ; else cout << (d[v] -= (dis[u] - dis[v])) << ' ' ; } cout << '\n' ; } }
稠密图——Floydf k , i → j f_{k,i\to j} f k , i → j 表示只经过节点1 ∼ k 1\sim k 1 ∼ k ,从i i i 到j j j 的最短路。
由d i → j = min k { d i → k + d k → j } d_{i \to j}=\min\limits_k \left\lbrace d_{i \to k}+d_{k \to j} \right\rbrace d i → j = k min { d i → k + d k → j } ,得f k , i → j = min { f k − 1 , i → j , f k − 1 , i → k + f k − 1 , k → j } f_{k,i\to j}=\min \left\lbrace f_{k-1,i \to j},\ f_{k-1,i \to k}+f_{k-1,k \to j} \right\rbrace f k , i → j = min { f k − 1 , i → j , f k − 1 , i → k + f k − 1 , k → j } 。第一维可压缩。
时间复杂度Θ ( n 3 ) \Theta(n^3) Θ ( n 3 ) ,空间复杂度Θ ( n 2 ) \Theta(n^2) Θ ( n 2 ) 。
1 2 3 4 5 6 7 8 9 10 11 12 auto Floyd = [&]() { auto dis = E; for (int u = 0 ; u < n; u++) dis[u][u] = 0 ; for (int k = 0 ; k < n; k++) { for (int u = 0 ; u < n; u++) { for (int v = 0 ; v < n; v++) dis[u][v] = min (dis[u][v], dis[u][k] + dis[k][v]); if (dis[u][u] < 0 ) return vector<vector<ll>>(); } } return dis; }
Problems 最短路计数更新时,若边权相同,则继承父节点的最短路数量;否则替换最短路数量。最短路计数会炸 int128,可以用 double 或者取模判断。
例 1 有公路和铁路,其中铁路由点1 1 1 引出,问至多删除多少条铁路不会影响从1 1 1 到任意点的最短路长度。(VJspr8H - Jzzhu and Cities )
思路 先用公路跑最短路,对铁路依次判断,若w ⩾ d v w\geqslant d_v w ⩾ d v ,则可以删除。
Hacked 4 3 1 2 1 2 3 3 3 4 2 2 3 3 4 5
应当用公路和铁路的所有路来跑最短路,再对铁路依次判断,若w > d v w>d_v w > d v ,则可以删除,若w = d v w=d_v w = d v ,且不是惟一最短路,则可以删除。
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 int n, m, k;cin >> n >> m >> k; vector<pair<int , int >> train; vector<vector<pair<int , int >>> E (n); while (m--) { int u, v, w; cin >> u >> v >> w; u--, v--; E[u].push_back ({ w, v }); E[v].push_back ({ w, u }); } while (k--) { int v, w; cin >> v >> w; v--; E[0 ].push_back ({ w, v }); E[v].push_back ({ w, 0 }); train.push_back ({ w, v }); } vector dis (n, Inf) ; vector<int > vis (n) ; vector<int > from (n, -1 ) ; vector<ll> pcnt (n) ; dis[0 ] = 0 , pcnt[0 ] = 1 ; priority_queue<pair<ll, int >, vector<pair<ll, int >>, greater<>> Q; Q.push ({ 0 , 0 }); while (!Q.empty ()) { auto [d, u] = Q.top (); Q.pop (); if (vis[u]) continue ; vis[u] = 1 ; for (auto & [w, v] : E[u]) { if (dis[v] > w + d) { dis[v] = w + d; from[v] = u; pcnt[v] = pcnt[u]; Q.push ({ dis[v], v }); } else if (dis[v] == w + d) { pcnt[v] += pcnt[u]; if (pcnt[v] > 1e18 ) pcnt[v] = 1e18 ; } } } int ans = 0 ;for (auto [w, v] : train) { if (w > dis[v]) ++ans; else if (w == dis[v] && pcnt[v] > 1 ) --pcnt[v], ++ans; } cout << ans << "\n" ;
例 2 无向图,有边权,m m m 次询问,每次询问删去第i i i 条边是否会影响1 − n 1-n 1 − n 最短路,询问间独立。n , m ⩽ 2 × 1 0 5 n ,m \leqslant 2\times 10^{5} n , m ⩽ 2 × 1 0 5 。(ABC375G - Road Blocked 2 )
思路 实际上是询问最短路是否必须经过u − v u-v u − v ,如果是,那必须长度一样,且路径唯一。具体来说,要满足d 1 u + w + d v n = d 1 n d_{1u}+w+d_{vn}=d_{1n} d 1 u + w + d v n = d 1 n ,以及p 1 u ⋅ p v n = p 1 n p_{1u}\cdot p_{vn}=p_{1n} p 1 u ⋅ p v n = p 1 n ,其中p p p 表示最短路数量。注意u u u 是靠近1 1 1 的那个点(也就是满足d 1 u < d 1 v d_{1u}<d_{1v} d 1 u < d 1 v )。
从1 1 1 和n n n 各跑一次最短路,同时最短路计数。最短路计数会炸 int128,可以用 double 或者取模判断。
时间复杂度Θ ( m log m ) \Theta(m\log m) Θ ( 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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 int n, m;cin >> n >> m; vector<vector<pair<int , int >>> E (n); vector<array<int , 3>> edge (m); for (int i = 0 ; i < m; i++) { int u, v, w; cin >> u >> v >> w; u--, v--; E[u].push_back ({ w, v }); E[v].push_back ({ w, u }); edge[i] = { u, v, w }; } auto Dijkstra = [&](int s) { vector dis (n, Inf); vector<int > vis (n) ; vector<int > from (n, -1 ) ; vector<ll> pcnt (n) ; dis[s] = 0 , pcnt[s] = 1 ; priority_queue<pair<ll, int >, vector<pair<ll, int >>, greater<>> Q; Q.push ({ 0 , s }); while (!Q.empty ()) { auto [d, u] = Q.top (); Q.pop (); if (vis[u]) continue ; vis[u] = 1 ; for (auto & [w, v] : E[u]) { if (dis[v] > w + d) { dis[v] = w + d; from[v] = u; pcnt[v] = pcnt[u]; Q.push ({ dis[v], v }); } else if (dis[v] == w + d) { (pcnt[v] += pcnt[u]) %= mod; } } } return make_pair (dis, pcnt); }; auto [d1, p1] = Dijkstra (0 );auto [dn, pn] = Dijkstra (n - 1 );for (auto & [u, v, w] : edge) { if (d1[u] > d1[v]) swap (u, v); bool only = (d1[u] + w + dn[v] == dn[0 ]) && (p1[u] * pn[v] % mod == pn[0 ]); cout << (only ? "Yes" : "No" ) << "\n" ; }
从起点和终点分别跑一边最短路例 1 n n n 点m m m 边无向图,k k k 个特殊点,需要连一条边,其两端都是特殊点,求所有连边方式中,从1 1 1 到n n n 的最短路的最大值。(CF1307D. Cow and Fields )
思路 设在u u u 和v v v 中连边,则经过这条边的最短路为min { d 1 u + 1 + d n v , d 1 v + 1 + d n u } \min\lbrace d_{1u}+1+d_{nv},\ d_{1v}+1+d_{nu} \rbrace min { d 1 u + 1 + d n v , d 1 v + 1 + d n u } ,所求即是
max 1 ⩽ u , v ⩽ n { min { d 1 u + 1 + d n v , d 1 v + 1 + d n u } } \max_{1 \leqslant u,v \leqslant n}\big\lbrace \min\lbrace d_{1u}+1+d_{nv},\ d_{1v}+1+d_{nu} \rbrace \big\rbrace 1 ⩽ u , v ⩽ n max { min { d 1 u + 1 + d n v , d 1 v + 1 + d n u } }
如果枚举u , v u,v u , v ,复杂度是Θ ( k 2 ) \Theta(k^{2}) Θ ( k 2 ) 的,需要优化。
不失一般性地假设d 1 u + 1 + d n v > d 1 v + 1 + d n u d_{1u}+1+d_{nv}> d_{1v}+1+d_{nu} d 1 u + 1 + d n v > d 1 v + 1 + d n u ,这样所求即是
max 1 ⩽ v ⩽ n , u ∈ S { min { d 1 v + 1 + d n u } } = max 1 ⩽ v ⩽ n { d 1 v + 1 + min u ∈ S { d n u } } \max_{1 \leqslant v \leqslant n,\ u\in S}\big\lbrace \min\lbrace d_{1v}+1+d_{nu} \rbrace \big\rbrace=\max_{1 \leqslant v \leqslant n}\big\lbrace d_{1v}+1+\min_{u\in S}\lbrace d_{nu} \rbrace \big\rbrace 1 ⩽ v ⩽ n , u ∈ S max { min { d 1 v + 1 + d n u } } = 1 ⩽ v ⩽ n max { d 1 v + 1 + u ∈ S min { d n u } }
式中,u u u 需满足d 1 u + 1 + d n v > d 1 v + 1 + d n u d_{1u}+1+d_{nv}> d_{1v}+1+d_{nu} d 1 u + 1 + d n v > d 1 v + 1 + d n u ,这等价于d 1 u − d n u > d 1 v − d n v d_{1u}-d_{nu}> d_{1v}-d_{nv} d 1 u − d n u > d 1 v − d n v ,两个变元是独立的,因此如果按照d 1 u − d n u d_{1u}-d_{nu} d 1 u − d n u 降序排序,遍历到v v v 时,所需用来更新的u u u 都是之前遍历过的,按主机派的思想,每遍历一个v v v ,先从集合中取出最小元素min u ∈ S { d n u } \min\limits_{u\in S}\lbrace d_{nu}\rbrace u ∈ S min { d n u } ,更新答案,再用d v u d_{vu} d v u 更新原集合,如此循环,时间复杂度Θ ( k ) \Theta(k) Θ ( k ) 。
时间复杂度Θ ( n + k log k ) \Theta(n+k\log k) Θ ( n + k log 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 60 61 62 63 void eachT () { int n, m, k; cin >> n >> m >> k; vector<int > tag (k) ; for (int i = 0 ; i < k; i++) { cin >> tag[i]; tag[i]--; } vector<vector<pii>> E (n); while (m--) { int u, v; cin >> u >> v; u--, v--; E[u].push_back ({ 1 , v }); E[v].push_back ({ 1 , u }); } auto Dijkstra = [&](int st) { vector dis (n, Inf); dis[st] = 0 ; vector<int > vis (n) ; using T = pair<ll, int >; priority_queue<T, vector<T>, greater<>> Q; Q.push ({ 0 , st }); while (!Q.empty ()) { auto [d, u] = Q.top (); Q.pop (); if (vis[u]) continue ; vis[u] = 1 ; for (auto & [w, v] : E[u]) { if (dis[v] > w + d) { dis[v] = w + d; Q.push ({ dis[v], v }); } } } return dis; }; auto d0 = Dijkstra (0 ); auto dn = Dijkstra (n - 1 ); using node = struct { ll d0, dn; }; vector<node> dp (k) ; for (int i = 0 ; i < k; i++) { dp[i].d0 = d0[tag[i]]; dp[i].dn = dn[tag[i]]; } sort (dp.begin (), dp.end (), [](node a, node b) { return a.d0 - a.dn > b.d0 - b.dn; }); ll ans = 0 , dnmx = dp[0 ].dn; for (int i = 1 ; i < k; i++) { ans = max (ans, dp[i].d0 + dnmx + 1 ); dnmx = max (dnmx, dp[i].dn); } ans = min (ans, dn[0 ]); cout << ans << '\n' ; }
边权改变问题——分层图分层图用于解决 边权改变 的最短路问题。层数k k k 不能太大,要求k n kn k n 在点数允许范围内,一般k ⩽ 10 k \leqslant 10 k ⩽ 1 0 。
第i i i 层图点u u u 的编号为i n + u in+u i n + u ,从第f f f 层图向第t t t 层图连边:
1 2 3 4 5 6 int N = n * k;vector<vector<pii>> E (N); auto add = [&](int f, int t, int u, int v, int w) { E[f * n + u].push_back ({ w, t * n + v }); E[f * n + v].push_back ({ w, t * n + u }); };
例 1 给定n n n 个城市,m m m 条双向道路,道路有通行费,且可以免费在最多k k k 条道路上免通行费。求出行最少花费。(洛谷 P4568 飞行路线 )
数据范围:2 ⩽ n ⩽ 1 0 4 2 \leqslant n \leqslant 10^4 2 ⩽ n ⩽ 1 0 4 ,1 ⩽ m ⩽ 5 × 1 0 4 1 \leqslant m \leqslant 5\times 10^4 1 ⩽ m ⩽ 5 × 1 0 4 ,0 ⩽ k ⩽ 10 0 \leqslant k \leqslant 10 0 ⩽ k ⩽ 1 0 ,0 ⩽ w ⩽ 1 0 3 0 \leqslant w \leqslant 10^3 0 ⩽ w ⩽ 1 0 3 。
思路 建k + 1 k+1 k + 1 层图,每层图向下一层图的边权为0 0 0 。
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 void eachT () { int n, m, k, st, ed; cin >> n >> m >> k >> st >> ed; ++k; int N = n * k; vector<vector<pii>> E (N); auto add = [&](int f, int t, int u, int v, int w) { E[f * n + u].push_back ({ w, t * n + v }); E[f * n + v].push_back ({ w, t * n + u }); }; while (m--) { int u, v, w; cin >> u >> v >> w; for (int i = 0 ; i < k; i++) add (i, i, u, v, w); for (int i = 1 ; i < k; i++) add (i - 1 , i, u, v, 0 ); } auto dis = Dijkstra (st); int ans = inf; for (int i = 0 ; i < k; i++) { ans = min (ans, dis[i * n + ed]); } cout << (ans == Inf ? -1 : ans) << '\n' ; }
例 2 给定一个包含n n n 个点,m m m 条带权无向边的图。规定若一条路径所包含的边边集为E E E ,那么这条路径的权值为∑ w i − max w i + min w i \sum w_i-\max w_i+\min w_i ∑ w i − max w i + min w i 。求从1 1 1 号点到每个点的最短路。
数据范围:2 ⩽ n ⩽ 2 × 1 0 5 , 1 ⩽ m ⩽ 2 × 1 0 5 , 0 ⩽ w i ⩽ 1 0 9 2 \leqslant n \leqslant 2\times10^5,\ 1 \leqslant m \leqslant 2\times10^5,\ 0 \leqslant w_i \leqslant 10^9 2 ⩽ n ⩽ 2 × 1 0 5 , 1 ⩽ m ⩽ 2 × 1 0 5 , 0 ⩽ w i ⩽ 1 0 9 。
思路 可以把路径的权值理解为:必须将路径上的任意一条边不算代价,任意一条边算 2 倍代价。这样我们就把对两条极值边的贡献方式转化成了所有边的贡献方式。更加统一,易于处理。
这两种特殊代价没有先后顺序,故建4 4 4 层图:1 → 2 1\to2 1 → 2 边权为0 0 0 ,2 → 4 2\to4 2 → 4 边权为2 w 2w 2 w ,1 → 3 1\to3 1 → 3 边权为2 w 2w 2 w ,3 → 4 3\to4 3 → 4 边权为0 0 0 。
答案为第1 1 1 层与第4 4 4 层图对应的答案的较小值。
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 void eachT () { int n, m; cin >> n >> m; int N = n * 4 ; vector<vector<pii>> E (N); auto add = [&](int f, int t, int u, int v, int w) { E[f * n + u].push_back ({ w, t * n + v }); E[f * n + v].push_back ({ w, t * n + u }); }; while (m--) { int u, v, w; cin >> u >> v >> w; u--, v--; for (int i = 0 ; i < 4 ; i++) add (i, i, u, v, w); add (0 , 1 , v, u, 0 ); add (1 , 3 , v, u, 2 * w); add (0 , 2 , v, u, 2 * w); add (2 , 3 , v, u, 0 ); } auto dis = Dijkstra (0 ); for (int i = 1 ; i < n; i++) { cout << min (dis[i], dis[3 * n + i]) << ' ' ; } }
例 3 骑马 CF2014E. ZRendez-vous de Marian et Robin
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 void eachT () { int n, m, h; cin >> n >> m >> h; vector<int > ishalf (n) ; for (int i = 0 ; i < h; i++) { int x; cin >> x; x--; ishalf[x] = 1 ; } vector<vector<pii>> E (n); while (m--) { int u, v, w; cin >> u >> v >> w; u--, v--; E[u].push_back ({ w, v }); E[v].push_back ({ w, u }); } auto Dijkstra = [&](int st) { vector dis (n, array{ Inf, Inf }); dis[st][0 ] = 0 ; vector vis (n, array{ 0 , 0 }) ; using T = tuple<ll, int , bool >; priority_queue<T, vector<T>, greater<>> Q; Q.push ({ 0 , st, 0 }); while (!Q.empty ()) { auto [d, u, half] = Q.top (); Q.pop (); if (vis[u][half]) continue ; vis[u][half] = 1 ; if (ishalf[u]) half = 1 ; for (auto [w, v] : E[u]) { if (half) w /= 2 ; if (dis[v][half] > d + w) { dis[v][half] = d + w; Q.push ({ dis[v][half], v, half }); } } } vector<ll> d (n) ; for (int i = 0 ; i < n; i++) { d[i] = min (dis[i][0 ], dis[i][1 ]); } return d; }; auto d0 = Dijkstra (0 ); auto dn = Dijkstra (n - 1 ); ll ans = Inf; for (int i = 0 ; i < n; i++) { ans = min (ans, max (d0[i], dn[i])); } cout << (ans == Inf ? -1 : ans) << '\n' ; }
例 4 2024 ICPC 网络赛 II E. Escape
机器人的活动范围距离起点的距离最大是d d d ,把所有起点扔到队列里,用 BFS 预处理找到到达每个点的步数。
对于某个点,玩家到达步数是x x x ,机器人到达步数是y y y ,则如果x x x 与y y y 的奇偶性相同,且x ⩾ y x \geqslant y x ⩾ y ,这个点就走不能经过。对于同一个点的x , y x,y x , y 可能有多个,只要存在一个x x x ,对于所有的y y y 都满足上述条件即可。因此只需与奇数y y y 或偶数y y y 的最小值比较。BFS 可以保证第一次访问的点就是最小值。
用 BFS 寻路。寻路的时候记录步数x x x ,与同奇偶性的y y y 比较,同时记录到达这个点的前驱,以输出路径。
每个点都可以经过两次,所以 vis
等数组都开成二维的,奇偶相互独立。
注意输出路径时不能写 if (u == 1) break
,可能玩家走 1->2->3->1
改变奇偶性,导致实际路径长度与输出的不符。在 BFS 里走到n n n 就输出返回是比较好的写法。
Bonus:如果数据有d = 0 d=0 d = 0 需特判,则不用考虑所有机器人,因为它们必定原地爆炸。
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 int n, m, D;cin >> n >> m >> D; vector<vector<int >> E (n); while (m--) { int u, v; cin >> u >> v; u--, v--; E[u].push_back (v); E[v].push_back (u); } queue<pair<int , int >> Q; vector val (n, array{ inf, inf }) ;int k;cin >> k; while (k--) { int x; cin >> x; x--; val[x][0 ] = 0 ; Q.push ({ 0 , x }); } while (!Q.empty ()) { auto [d, u] = Q.front (); Q.pop (); if (d >= D) continue ; ++d; for (auto & v : E[u]) { if (val[v][d & 1 ] != inf) continue ; val[v][d & 1 ] = d; Q.push ({ d, v }); } } vector from (n, array{ -1 , -1 }) ;Q.push ({ 0 , 0 }); while (!Q.empty ()) { auto [d, u] = Q.front (); Q.pop (); if (u == n - 1 ) { cout << d << '\n' ; vector<int > path; while (d >= 0 ) { path.push_back (u); u = from[u][d & 1 ]; d--; } for (int i = path.size () - 1 ; i >= 0 ; --i) { cout << path[i] + 1 << ' ' ; } cout << '\n' ; return 0 ; } ++d; for (auto & v : E[u]) { if (d >= val[v][d & 1 ]) continue ; if (~from[v][d & 1 ]) continue ; from[v][d & 1 ] = u; Q.push ({ d, v }); } } cout << "-1\n" ;
例 5 Alice 和 Bob 在一张简单无向图上博弈,Alice 先手。每次每人必须沿着当前所在的点的一条边走到另一端,不能走到对手所在的位置,不能行动者输。对于每个i i i 从1 1 1 到n − 1 n-1 n − 1 ,求出 Alice 初始在0 0 0 号点,Bob 初始在i i i 号点,谁会赢或平局。(SunBoYi [[…/contests/2024 杭电多校 /2024-08-18:2024“钉耙编程”中国大学生算法设计超级联赛(10)|2024-08-18:2024“钉耙编程”中国大学生算法设计超级联赛(10)]])
首先特判两人在两个连通块里的情况,是简单的,除了孤立点的情况外,必然可以一直沿着一条边反复横跳无限走。接下来默认两人在同一连通块。
不难发现,如果一个人走到了环上,那么他就立于不败之地。同样不难发现,如果一根人走到了连接两个环的一根链上,他也不会输,因为其两边必然只能有最多一边可能被对手阻挡,那么他一定能走到一个环上。我们称这两类点为好点。
所以我们对于两人分别求出,自己能否阻挡对方进入某个好点。
首先先判断掉该连通块是树的情况,此时不存在好点,胜负只与初始 Alice 和 Bob 的距离的奇偶性有关。
否则,我们先求出好点,用类似基环树和拓扑排序的方法剥一度点,剩下的都是好点。
对于一个人,考虑为了阻挡对手进入好点,他必须尽快赶到离对手最近的好点。同时,与树的情况类似的,还需要保持距离的奇偶性正确,才能把对手逼到某个一度点,不然他会被迫后退,让对手走到好点。所以相当于要求他的初始位置到离对手最近的好点,且奇偶性给定的最短路长度,并于对手要走的距离进行比较。由于起点或终点固定,故可以分层图 BFS 解决。
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 int n, m;cin >> n >> m; DSU dsu (n) ;vector<vector<int >> E (n); while (m--) { int u, v; cin >> u >> v; u--, v--; E[u].push_back (v); E[v].push_back (u); dsu.uno (u, v); } string ans (n - 1 , '?' ) ;for (int i = 1 ; i < n; i++) { if (!dsu.same (i, 0 )) { if (dsu.size (0 ) == 1 ) ans[i] = 'B' ; else if (dsu.size (i) == 1 ) ans[i] = 'A' ; else ans[i] = 'D' ; } } vector<int > dis (n) , vis (n) ;auto dfs = [&](this auto && self, int u, int p) { vis[u] = true ; for (auto & v : E[u]) { if (v == p) continue ; if (vis[v]) return false ; dis[v] = dis[u] + 1 ; if (!self (v, u)) return false ; } return true ; }; if (dfs (0 , -1 )) { for (int i = 1 ; i < n; i++) { if (dsu.same (i, 0 )) ans[i] = dis[i] & 1 ? 'B' : 'A' ; } } else { vector<int > deg (n); SSN ssn (n) ; queue<int > Q; for (int i = 0 ; i < n; i++) { if (dsu.same (i, 0 )) { deg[i] = E[i].size (); if (deg[i] == 1 ) Q.push (i); } } while (!Q.empty ()) { int u = Q.front (); Q.pop (); for (auto & v : E[u]) { ssn.uno (v, u, 1 ); if (--deg[v] == 1 ) Q.push (v); } } auto bfs = [&](int st) { vector dis (n, array{ inf, inf }); queue<pair<int , int >> Q; dis[st][0 ] = 0 ; Q.push ({ st, 0 }); while (Q.size ()) { auto [u, d] = Q.front (); Q.pop (); for (auto & v : E[u]) { if (dis[v][!d] != inf) continue ; dis[v][!d] = dis[u][d] + 1 ; Q.push ({ v, !d }); } } return dis; }; auto d0 = bfs (0 ); auto dp = bfs (ssn.find (0 )); for (int i = 1 ; i < n; i++) { if (dsu.same (i, 0 )) { if (d0[ssn.find (i)][ssn.depth (i) & 1 ] <= ssn.depth (i)) ans[i] = 'A' ; else if (dp[i][!(ssn.depth (0 ) & 1 )] <= ssn.depth (0 ) - 1 ) ans[i] = 'B' ; else ans[i] = 'D' ; } } } for (int i = 1 ; i < n; i++) { cout << ans[i]; } cout << "\n" ;
建立虚点连边坐地铁,求最少搭乘几种线路,即求最小地铁换乘次数 + 1。(2260, CF1941G. Rudolf and Subway )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int n, m;cin >> n >> m; vector<vector<int >> E (n + m); while (m--) { cin >> u >> v >> c; u--, v--, c += n; E[c].push_back (u), E[u].push_back (c); E[c].push_back (v), E[v].push_back (c); } int s, t;cin >> s >> t; s--, t--; cout << (bfs (s)[t] / 2 ) << "\n" ;
刷微信步数——倍增优化 Floyd题意 有向图,每条边的边权为 1km,走路的速度为每单位时间 1km,空间跑路器为每单位时间2 k 2^k 2 k km(k k k 是任意自然数),求从点1 1 1 到点n n n 的最短时间。数据范围:2 ⩽ n ⩽ 50 2 \leqslant n \leqslant 50 2 ⩽ n ⩽ 5 0 ,m ⩽ 1 0 4 m \leqslant 10 ^ 4 m ⩽ 1 0 4 ,保证有解。(VJsum4A - 洛谷 P1613 跑路 )
思路 如果有u , v u,v u , v 两点间的最短距离为2 k 2^k 2 k ,可认为这两点之间有一条边。设f i , j , k f_{i,j,k} f i , j , k 表示能否在2 i 2^i 2 i 时间内从点j j j 走到点k k k ,则
f i , v , u ← { f i − 1 , v , k ⊗ f i − 1 , k , u , i > 0 E u , v , i = 0 f_{i, v, u}\leftarrow \begin{cases} f_{i-1, v, k} \otimes f_{i-1, k, u},& i>0\\ E_{u,v},& i=0 \end{cases} f i , v , u ← { f i − 1 , v , k ⊗ f i − 1 , k , u , E u , v , i > 0 i = 0
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 n, m;cin >> n >> m; vector f (D, vector(n, vector<int >(n))) ;while (m--) { int u, v; cin >> u >> v; u--, v--; f[0 ][u][v] = 1 ; } for (int bit = 1 ; bit < D; bit++) { for (int k = 0 ; k < n; ++k) for (int u = 0 ; u < n; u++) for (int v = 0 ; v < n; v++) f[bit][u][v] |= f[bit - 1 ][u][k] & f[bit - 1 ][k][v]; } vector adj (n, vector(n, inf)) ;for (int u = 0 ; u < n; u++) { for (int v = 0 ; v < n; v++) for (int bit = 0 ; bit < D; bit++) if (f[bit][u][v]) adj[u][v] = 1 ; } for (int k = 0 ; k < n; ++k) { for (int u = 0 ; u < n; u++) for (int v = 0 ; v < n; v++) adj[u][v] = min (adj[u][v], adj[u][k] + adj[k][v]); } cout << adj[0 ][n - 1 ] << "\n" ;
恰好 k 边最短路给定无向图,求从起点到终点的恰好经过k k k 条边的最短路。数据范围:边数m ⩽ 100 m \leqslant 100 m ⩽ 1 0 0 ,2 ⩽ k ⩽ 1 0 6 2 \leqslant k \leqslant 10^{6} 2 ⩽ k ⩽ 1 0 6 ,保证有解。(AcWing345. 牛站 , 洛谷 P2886 Cow Relays G )
矩阵快速幂优化 Floyd设f i , u , v f_{i,u,v} f i , u , v 表示恰好经过i i i 条边的最短路,则
f i , u , v ← { f i − 1 , u , k + E k , v , i > 1 E u , v , i = 1 f_{i, u, v}\leftarrow \begin{cases} f_{i-1, u, k} +E_{k,v},& i>1\\ E_{u, v},& i=1 \end{cases} f i , u , v ← { f i − 1 , u , k + E k , v , E u , v , i > 1 i = 1
时间复杂度为Θ ( n 3 k ) \Theta(n^{3}k) Θ ( n 3 k ) ,需要快速计算f i , u , v f_{i,u,v} f i , u , v 。注意到,上式具有结合律,可以使用倍增优化。最终时间复杂度为Θ ( n 3 log k ) \Theta(n^{3}\log k) Θ ( n 3 log 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 using Matrix = vector<vector<int >>;Matrix mul (Matrix& a, Matrix& b) { const int n = a.size (); Matrix res (n, vector<int >(n, inf)) ; for (int k = 0 ; k < n; ++k) { for (int u = 0 ; u < n; u++) { for (int v = 0 ; v < n; v++) { res[u][v] = min (res[u][v], a[u][k] + b[k][v]); } } } return res; } Matrix qpow (Matrix& a, int n) { auto res = a; --n; while (n) { if (n & 1 ) res = mul (res, a); a = mul (a, a); n >>= 1 ; } return res; } int main () { int k, m; cin >> k >> m; int n = 0 ; unordered_map<int , int > mp; auto get = [&](int u) { if (!mp.count (u)) mp[u] = n++; return mp[u]; }; int s, t; cin >> s >> t; s = get (s), t = get (t); vector adj (m, vector(m, inf)) ; while (m--) { int u, v, w; cin >> w >> u >> v; u = get (u), v = get (v); adj[u][v] = adj[v][u] = min (adj[u][v], w); } auto res = qpow (adj, k); cout << res[s][t] << "\n" ; return 0 ; }
强制转移的 Bellmon-Ford时间复杂度Θ ( k m ) \Theta(km) Θ ( k m ) 。
1 2 3 4 5 6 7 8 9 10 11 vector dis (n, inf) ;dis[s] = 0 ; for (int t = 0 ; t < k; t++) { vector ndis (n, inf) ; for (auto & [w, u, v] : E) { ndis[u] = min (ndis[u], dis[v] + w); ndis[v] = min (ndis[v], dis[u] + w); } dis = ndis; } cout << dis[t] << endl;
一种看不懂的做法:
当N N N 很大的时候,最优策略一定是在某个边权很小的边上绕圈。
性质 1 :只有可能在路径上一条最短的边上连续走多次。
证明 :假如在其他边上连续走了多次,那我们可以将路径上来回走的一对边( u → v , v → u ) (u\to v,v\to u) ( u → v , v → u ) 删去,换成在最短的边上来回走,这样一定不劣。
性质 2 :只有路径上的一条最短边可能被在同一方向经过三次及以上,这里“经过”不要求连续。
证明 :假如另外一条边被在同一方向经过三次及以上,那么路径上一定有包含这条边的两个环。若这两个环中有至少一个长度为偶数,那么可以删去这个环并在最短边上来回走;否则,两个环的长度都是奇数,把两个环都删去,换成在最短边上来回走即可。
结论 :最优方案下,只会有至多一条边被经过> 2 >2 > 2 次。
设f u , d f_{u,d} f u , d 表示从S S S 到u u u ,经过的边数为d d d 的最短路。由于每条边都被经过≤ 2 \le 2 ≤ 2 次,只需算出d ≤ 2 T d\le 2T d ≤ 2 T 的f f f 即可。于是可以O ( T 2 ) \mathcal{O}(T^2) O ( T 2 ) 预处理。同理,预处理g u , d g_{u,d} g u , d 表示从E E E 的。
枚举每条边作为那条被多次经过的边,并O ( T ) \mathcal{O}(T) O ( T ) 算出最小值即可。具体计算方式可见代码中的 Calc
函数。
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 #include <iostream> #include <algorithm> #include <vector> #include <unordered_map> using namespace std;typedef long long ll;const int inf = 0x3f3f3f3f ;signed main () { int k, m; cin >> k >> m; int n = 0 ; unordered_map<int , int > mp; auto get = [&](int u) { if (!mp.count (u)) mp[u] = n++; return mp[u]; }; int s, t; cin >> s >> t; s = get (s), t = get (t); vector<tuple<int , int , int >> E (m); for (auto & [w, u, v] : E) { cin >> w >> u >> v; u = get (u), v = get (v); } auto bfs = [&](int s) { vector dis (m, vector (m + 1 << 1 , inf)); dis[s][0 ] = 0 ; for (int i = 1 ; i <= (m << 1 | 1 ); i++) { for (auto & [w, u, v] : E) { dis[u][i] = min (dis[u][i], dis[v][i - 1 ] + w); dis[v][i] = min (dis[v][i], dis[u][i - 1 ] + w); } } return dis; }; auto ds = bfs (s), dt = bfs (t); auto Calc = [&](int u, int w) { int res = inf; for (int c = 0 ; c <= 1 ; c++) { int mn = inf; for (int j = m * 2 + c, l = (k + c) % 2 - 2 ; j >= 0 ; j -= 2 ) { while (l < m * 2 && k - j - l>1 ) { l += 2 ; mn = min (mn + 2 * w, dt[u][l]); } if (mn >= inf || ds[u][j] >= inf) continue ; res = min (res, mn + ds[u][j] + (k - j - l) * w); } } return res; }; int ans = inf; for (auto & [w, u, v] : E) { ans = min (ans, min (Calc (u, w), Calc (v, w))); } cout << ans << endl; return 0 ; }
建模例 1 小红拿到了一个数组,她可以进行若干次以下操作:
选择一个元素,花费p p p ,使其加x = 1 x=1 x = 1 。 选择一个元素,花费q q q ,使其减y y y 。 希望若干次操作后,数组的平均数是一个整数,求最小总代价。(小红的数组操作 )
方法一 直接算,注意可以减为负数,所以循环从1 1 1 到n n n 要跑满:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int n, p, x, q, y;cin >> n >> p >> x >> q >> y; int sum = 0 ;for (int i = 0 ; i < n; i++) { int x; cin >> x; (sum += x) %= n; } ll min = 1e18 ; for (int i = 0 ; i < n; i++) { min = std::min (min, 1ll * q * i + p * (n - (sum - 1ll * i * y - 1 ) % n - 1 )); } cout << min << "\n" ;
这种方法在 Hard 版本仍然使用,不同的是,我们需要知道每次加x x x ,得到目标k k k 的最小次数。有两种方案可以求:
a x ≡ b ( m o d n ) ax≡b \pmod n a x ≡ b ( m o d n ) ,可以直接使用 exgcd 来解决。开个数组直接预处理出k x kx k x 在模n n n 意义下的值,这样即可 O(1) 调用了。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int n, p, x, q, y;cin >> n >> p >> x >> q >> y; int sum = 0 ;for (int i = 0 ; i < n; i++) { int x; cin >> x; (sum += x) %= n; } vector mp (n, Inf) ;for (int i = n - 1 ; i >= 0 ; i--) { mp[(1ll * i * x + sum) % n] = 1ll * i * p; } ll res = Inf; for (int i = 0 ; i <= n; i++) { res = min (res, mp[1ll * i * y % n] + 1ll * i * q); } cout << (res == Inf ? -1 : res);
方法二 求最小代价,可以联想到最短路:
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 n, p, x, q, y;cin >> n >> p >> x >> q >> y; int s = 0 ;for (int i = 0 ; i < n; i++) { int x; cin >> x; (s += x) %= n; } vector dis (n, Inf) ;vector<bool > vis (n) ;priority_queue<pair<ll, int >, vector<pair<ll, int >>, greater<>> Q; Q.push ({ 0 , s }); dis[s] = 0 ; while (!Q.empty ()) { auto [d, u] = Q.top (); Q.pop (); if (vis[u]) continue ; vis[u] = 1 ; int v1 = ((u + x) % n + n) % n; if (dis[v1] > dis[u] + p) { dis[v1] = dis[u] + p; Q.push ({ dis[v1], v1 }); } int v2 = ((u - y) % n + n) % n; if (dis[v2] > dis[u] + q) { dis[v2] = dis[u] + q; Q.push ({ dis[v2], v2 }); } } cout << (dis[0 ] == Inf ? -1 : dis[0 ]) << endl;
例 2 n n n 个问题编号从1 1 1 到n n n ,每个问题有得分a i a_i a i 和参数b i b_i b i 。
从第一个问题开始回答,按如下次序回答每个问题,两种选择:
提交问题并获得a i a_i a i 分,然后问题作废,下一题的下标j j j 是满足j < i j<i j < i 且问题未作废的最大下标; 跳过问题,然后问题作废,下一题的下标j j j 是满足j ⩽ b i j \leqslant b_{i} j ⩽ b i 且问题未作废的最大下标。 最大化得分。(CF2023B. Skipping )
这个「下一题」类似于图论中的有向边,最大化得分即是最长路,考虑建图。
i → b i i\to b_{i} i → b i 连边权为− a i -a_{i} − a i 的边,表示跳过问题;i → i − 1 i\to i-1 i → i − 1 连边权为0 0 0 的边,表示提交问题;i → t i\to t i → t 连边权为∑ j = 1 i a j \sum_{j=1}^{i} a_{j} ∑ j = 1 i a j 的边,表示不再跳过问题,按顺序回答前面的每个问题直至结束。(其中t t t 是汇点)跑1 → t 1\to t 1 → t 的最长路即是答案。共3 n 3n 3 n 条边,时间复杂度Θ ( n log n ) \Theta(n\log n) Θ ( n log n ) 。
例 3 Tenzing 有n n n 个朋友,每次举办聚会可以邀请一些朋友,有如下限制:
1 1 1 必须参加,n n n 不能参加。有k k k 条限制( x , y , z ) (x, y, z) ( x , y , z ) ,表示x x x 和y y y 中只有一个在聚会中的总时间不超过z z z 。 最大化聚会总时间,并给出相应方案,即给出总聚会时间,每次聚会给出参与聚会的人和单次聚会时长。n ⩽ 2 × 1 0 5 n \leqslant 2\times 10^{5} n ⩽ 2 × 1 0 5 。(CF1842D. Tenzing and His Animal Friends )
矛盾冲突点在n n n ,所以需要找一条路径,路径上u − v u-v u − v 表示u u u 参加而v v v 不参加,这样从1 1 1 一直推到n n n ,进一步分析知是最短路,按 BFS 构造,细节很多。
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 n, m;cin >> n >> m; vector<vector<pair<int , int >>> E (n); while (m--) { int u, v, w; cin >> u >> v >> w; u--, v--; E[u].push_back ({ w, v }); E[v].push_back ({ w, u }); } auto dis = Dijkstra (0 );if (dis[n - 1 ] == Inf) { cout << "inf\n" ; } else { vector<int > pos (n); iota (pos.begin (), pos.end (), 0 ); sort (pos.begin (), pos.end (), [&](const int & a, const int & b) { return dis[a] == dis[b] ? a < b : dis[a] < dis[b]; }); vector<pair<string, ll>> res; string s (n, '0' ) ; for (int i = 0 ; i < n; i++) { if (pos[i] == n - 1 ) break ; s[pos[i]] = '1' ; res.push_back ({ s, dis[pos[i + 1 ]] - dis[pos[i]] }); } cout << dis[n - 1 ] << " " << res.size () << "\n" ; for (auto & [s, d] : res) { cout << s << " " << d << "\n" ; } }
杂题题意 无向图,有边权,两种操作:
永久删去一条边; 询问u , v u,v u , v 最短路。 数据范围:n ⩽ 300 , q ⩽ 2 × 1 0 5 , q 1 ⩽ 300 n \leqslant 300,\ q \leqslant 2\times 10^{5}, \ q_{1} \leqslant 300 n ⩽ 3 0 0 , q ⩽ 2 × 1 0 5 , q 1 ⩽ 3 0 0 。(ABC375F - Road Blocked )
思路 对于操作一,跑 300 次全源最短路时间是不够的,而且这加强了问题。应当考虑删边的影响。
删边不好做,倒着处理询问,转化为加边。设加上了s − t s-t s − t 边,那么u − v u-v u − v 最短路可能会更新,如果更新那必须经过s − t s-t s − t 边,路径长度是d u − s + w + d t − v d_{u-s}+w+d_{t-v} d u − s + w + d t − v 或d u − t + w + d s − v d_{u-t}+w+d_{s-v} d u − t + w + d s − v 。遍历u , v u,v u , v 更新最短路。时间复杂度Θ ( n 3 + q 1 n 2 ) \Theta(n^{3}+q_{1}n^{2}) Θ ( n 3 + q 1 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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 int n, m, q;cin >> n >> m >> q; vector<array<int , 3>> E (m); for (int i = 0 ; i < m; i++) { int u, v, w; cin >> u >> v >> w; u--, v--; E[i] = { u, v, w }; } vector<array<int , 3>> que (q); vector<int > del (m) ;for (int i = 0 ; i < q; i++) { int op; cin >> op; if (op == 1 ) { int x; cin >> x; x--; del[x] = 1 ; que[i] = { op, x, 0 }; } else { int u, v; cin >> u >> v; u--, v--; que[i] = { op, u, v }; } } vector dis (n, vector(n, Inf)) ;for (int u = 0 ; u < n; u++) { dis[u][u] = 0 ; } for (int i = 0 ; i < m; i++) { if (!del[i]) { auto [u, v, w] = E[i]; dis[u][v] = dis[v][u] = w; } } for (int k = 0 ; k < n; ++k) { for (int u = 0 ; u < n; u++) { for (int v = 0 ; v < n; v++) { dis[u][v] = min (dis[u][v], dis[u][k] + dis[k][v]); } } } vector<ll> ans (q) ;for (int i = q - 1 ; i >= 0 ; i--) { auto [op, a, b] = que[i]; if (op == 1 ) { auto [s, t, w] = E[a]; for (int u = 0 ; u < n; u++) { for (int v = 0 ; v < n; v++) dis[u][v] = min (dis[u][v], min (dis[u][s] + dis[t][v] + w, dis[u][t] + dis[s][v] + w)); } } else { ans[i] = dis[a][b]; } } for (int i = 0 ; i < q; i++) { if (ans[i]) cout << (ans[i] == Inf ? -1 : ans[i]) << "\n" ; }
点权最短路给出一张每个顶点v v v 带有颜色c v c_v c v 和点权w v w_v w v 的无向图。顶点有激活和未激活两种状态, 最开始每个顶点都是未激活的,激活一个顶点v v v 需要花费的代价为其点权w v w_v w v 。
你有一个棋子, 棋子只能放在 / 移动到被激活的顶点上。你可以随时选择一种颜色c c c , 回收这种颜色的所有已激活顶点的激活代价并将其全部设为未激活。当然,c c c 不能是你棋子当前所在顶点的颜色, 因为棋子只能放在已激活顶点上。
现在对于图中的每一对顶点( s , t ) (s, t) ( s , t ) , 询问若最开始图中所有点均未被激活, 从s s s 出发走到t t t 所需的最小初始点权是多少。
数据范围:n ⩽ 300 n \leqslant 300 n ⩽ 3 0 0 。
思路 考虑答案形态。
注意到每走到一个不同颜色的顶点上时你都可以回收其他颜色的所有顶点的点权。将答案(一条路径,视为顶点序列) 按颜色分段,则其所需的最小初始点权为“每一段的点权和加上下一段的第一个顶点的点权”的最大值。
于是我们可以的将图按颜色分为多个同色连通块,每块内部用 Floyd-Warshall 求两两之间最小点权路径。
然后通过枚举每段的起点,终点,以及下一段的起点建出一 张带边权的有向图,边权即为连接起点与终点的最小点权路径的点权之和加上下一段起点的点权。
最后在这张有向图上从每个顶点出发使用类似 Prim 的做法(每轮选择一个未连通的点权最小的出点)找出以每个点 s 为根的有向瓶颈生成树。
时间复杂度Θ ( n 3 ) \Theta(n^{3}) Θ ( n 3 ) 。
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 #include <iostream> #include <vector> using namespace std;using ll = long long ;const ll Inf = 0x3f3f3f3f3f3f3f3f ;signed main () { ios::sync_with_stdio (false ); cin.tie (nullptr ); cout.tie (nullptr ); int t; cin >> t; while (t--) { int n, m; cin >> n >> m; vector<int > c (n) , w (n) ; for (int i = 0 ; i < n; i++) { cin >> c[i]; } for (int i = 0 ; i < n; i++) { cin >> w[i]; } vector adj (n, vector<int >(n)) ; vector dis (n, vector<ll>(n, Inf)) ; while (m--) { int u, v; cin >> u >> v; u--, v--; adj[u][v] = adj[v][u] = 1 ; if (c[u] == c[v]) { dis[u][v] = dis[v][u] = w[u] + w[v]; } } for (int u = 0 ; u < n; u++) { dis[u][u] = w[u]; } for (int k = 0 ; k < n; ++k) { for (int u = 0 ; u < n; u++) { if (c[u] != c[k]) continue ; for (int v = 0 ; v < n; v++) { if (c[v] != c[u]) continue ; dis[u][v] = min (dis[u][v], dis[u][k] + dis[k][v] - w[k]); dis[v][u] = min (dis[v][u], dis[v][k] + dis[k][u] - w[k]); } } } for (int u = 0 ; u < n; u++) { for (int v = 0 ; v < n; v++) { if (c[u] != c[v]) continue ; for (int s = 0 ; s < n; ++s) { if (c[v] != c[s] && adj[v][s]) dis[u][s] = min (dis[u][s], dis[u][v] + w[s]); } } } for (int s = 0 ; s < n; ++s) { vector<ll> mn (n, Inf) , dp (n, Inf) , ndp (n, Inf) ; vector<int > vis (n) ; dp[s] = mn[s] = 0 ; for (int t = 0 ; t < n; t++) { int u = -1 ; for (int j = 0 ; j < n; j++) { if (!vis[j] && (u == -1 || mn[j] < mn[u])) { u = j; } } if (u == -1 ) break ; vis[u] = 1 ; for (int v = 0 ; v < n; v++) { if (!vis[v] && c[u] != c[v] && dis[u][v] < mn[v]) { mn[v] = dis[u][v]; dp[v] = max (dp[u], mn[v]); } } } for (int u = 0 ; u < n; u++) { for (int v = 0 ; v < n; v++) { if (c[u] == c[v]) ndp[v] = min (ndp[v], max (dp[u], dis[u][v])); } } for (int u = 0 ; u < n; u++) { dp[u] = min (dp[u], ndp[u]); } for (int u = 0 ; u < n; u++) { cout << dp[u] << " \n" [u == n - 1 ]; } } } return 0 ; }