最小生成树相关问题
最小生成树问题经常要求:点 ( u , v ) (u, v) ( u , v ) 在 MST \textbf{MST} MST 上,权值最大的边 maxcost ( u , v ) \text{maxcost}(u, v) maxcost ( u , v )
实现的过程仍然是基于 dfs 和 dp 的,一般来说
1 2 3 4 5 定义数据结构 vector<Edge> maxcost[maxn][maxn]; 另外需要一个集合 nodes 维护 dfs 时已经被访问过的节点 vector<int> nodes;
执行 dfs ( u , f a ) \textbf{dfs}(u, fa) dfs ( u , f a ) ,尝试加入当前点 u u u
遍历已访问过的节点集合 nodes \text{nodes} nodes ,∀ x ∈ nodes \forall x \in \text{nodes} ∀ x ∈ nodes ,更新 maxcost ( x , u ) \text{maxcost}(x, u) maxcost ( x , u ) ,检查路径 x → f a → u x \to fa \to u x → f a → u
如果 maxe ( x , f a ) . z > w ( f a , u ) \text{maxe}(x, fa).z > w(fa, u) maxe ( x , f a ) . z > w ( f a , u ) ,那么 maxe ( x , u ) ← maxe ( x , f a ) \text{maxe}(x, u) \leftarrow \text{maxe}(x, fa) maxe ( x , u ) ← maxe ( x , f a )
否则的话,maxe ( u , x ) ← E d g e ( f a , u , w ( f a , u ) ) \text{maxe}(u, x) \leftarrow \bm{Edge}(fa, u, w(fa, u)) maxe ( u , x ) ← E d g e ( f a , u , w ( f a , u ) )
将 u u u 加入集合 nodes \text{nodes} nodes 中,然后 dfs ( v , u ) \text{dfs}(v, u) dfs ( v , u )
Qin Shi Huang’s National Road System
根据 n n n 的取值范围,该问题可以暴力枚举 ( i , j ) (i, j) ( i , j ) ,然后求 A ( i , j ) / B ( i , j ) A(i, j) / B(i, j) A ( i , j ) / B ( i , j ) 的最大值
其中 A ( i , j ) A(i, j) A ( i , j ) 很容易求,就是 i , j i, j i , j 两座城市的人口和 p i + p j p_i + p_j p i + p j
那么 B ( i , j ) B(i, j) B ( i , j ) 呢?首先如果不用法术,B B B 一定是原图中的最小生成树
用法术,B ( i , j ) B(i, j) B ( i , j ) 最小值,一定是 mst − maxcost ( i , j ) \text{mst} - \text{maxcost}(i, j) mst − maxcost ( i , j )
i , j i, j i , j 沿着最小生成树走,经过的最大的边记为 maxcost ( i , j ) \text{maxcost}(i, j) maxcost ( i , j ) ,将其从最小生成树上删去,就是答案
maxcost ( i , j ) \text{maxcost}(i, j) maxcost ( i , j ) 可以通过 O ( n 2 ) O(n^2) O ( n 2 ) 的预处理求出
核心预处理 maxcost \text{maxcost} maxcost 的代码给出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct Edge { int x, y; double z; }; vector<vector<Edge> > maxe(maxn, vector<Edge>(maxn, Edge())); vector<int> nodes; void dfs(int u, int fa) { for (auto x : nodes) { if (maxe[x][fa].z > g[fa][u]) maxe[x][u] = maxe[u][x] = maxe[x][fa]; else maxe[x][u] = maxe[u][x] = Edge{fa, u, g[fa][u]}; } nodes.push_back(u); for (auto v : G[u]) { if (!used[u][v] || v == fa) continue ; dfs(v, u); } }
增量最小生成树
包含 n n n 个点的图,依次加入 m m m 条带权边,每加入一条边,输出当前图中最小生成树的权值
先求出原图 G G G 的一个最小生成树,加入边 e ( u , v ) e(u, v) e ( u , v ) 之后,图中包含 u → v u \to v u → v 的一个环
因此需要找出 MST \text{MST} MST 中 u → v u \to v u → v 路径上权值最大的边,即 maxcost ( u , v ) \text{maxcost}(u, v) maxcost ( u , v )
最后的答案为 r e s = MST − maxcost ( u , v ) + min ( maxcost ( u , v ) , e ( u , v ) ) res = \text{MST} - \text{maxcost}(u, v) + \min(\text{maxcost}(u, v), e(u, v)) r e s = MST − maxcost ( u , v ) + min ( maxcost ( u , v ) , e ( u , v ) )
RMQ 解决树上问题
最小生成树有关的问题中,经常求 MST \text{MST} MST 中 u → v u \to v u → v 的最大值或最小值
如果将生成树伸展成链,问题可以抽象为区间最值问题 ,可以用 RMQ 算法求解
RMQ 算法是基于倍增的思想,假设原数组为 A [ ⋯ ] A[\cdots] A [ ⋯ ] ,RMQ 需要用一个新数组 f ( i , j ) f(i, j) f ( i , j )
表示从 i i i 开始,长度为 2 j 2^{j} 2 j 的一段元素的最小值
可以写出递推关系 f ( i , j ) = min ( f ( i , j − 1 ) , f ( i + 2 j − 1 , j − 1 ) ) f(i, j) = \min (f(i, j-1), f(i+2^{j-1}, j-1)) f ( i , j ) = min ( f ( i , j − 1 ) , f ( i + 2 j − 1 , j − 1 ) )
初始化时,令 f ( i , 0 ) = A [ i ] f(i, 0) = A[i] f ( i , 0 ) = A [ i ]
RMQ 的查询操作也很简单,令 k k k 为满足 2 k ⩽ R − L + 1 2^{k} \leqslant R-L+1 2 k ⩽ R − L + 1 的最大整数,那么区间最值为
min ( f ( l , k ) , f ( r − 2 k + 1 , k ) ) \min (f(l, k), f(r-2^k+1, k)) min ( f ( l , k ) , f ( r − 2 k + 1 , k ) )
Frequent Values
注意到整个数组 A i A_i A i 都是非降序的,那么可以用分块思想 ,将相同的数合并成一个块
对于某个位置 i i i ,l e ( i ) le(i) l e ( i ) 表示 i i i 的最左边,和 A i A_i A i 相同的元素的下标
同理 r i ( i ) ri(i) r i ( i ) 表示最右边和 i i i 相同的元素的下标
对于区间 ( l , r ) (l, r) ( l , r ) ,求 ( l , r ) (l, r) ( l , r ) 中出现次数最多的元素,出现的次数
最多元素出现次数是以下几部分取 max \max max
[ l , r i ( l ) ] [l, ri(l)] [ l , r i ( l ) ] ,[ l e ( r ) , r ] [le(r), r] [ l e ( r ) , r ] ,以及中间的一部分
[ l , r i ( l ) ] [l, ri(l)] [ l , r i ( l ) ] 很好求,r i ( l ) − l + 1 ri(l) - l + 1 r i ( l ) − l + 1 就是答案,另外 l e ( i ) le(i) l e ( i ) 和 r i ( i ) ri(i) r i ( i ) 数组也很容易求
只需要 O ( n ) O(n) O ( n ) 从前往后扫描整个 A [ ⋯ ] A[\cdots] A [ ⋯ ] ,对于某个位置 i i i
如果 A i = A i − 1 A_i = A_{i-1} A i = A i − 1 ,那么 l e ( i ) = l e ( i − 1 ) le(i) = le(i-1) l e ( i ) = l e ( i − 1 ) ,否则的话 l e ( i ) = i le(i) = i l e ( i ) = i
那么中间部分怎么求呢?实际上中间部分只需要缩点成块 即可,将相同的数缩成一个个块,并且记录块编号
然后对块执行 RMQ 即可,具体编程实现如下
对于某一个位置 i i i ,n u m ( i ) num(i) n u m ( i ) 表示 i i i 所在块的编号
如果 A i = A i − 1 A_i = A_{i-1} A i = A i − 1 ,那么不需要开新块 ,直接令 n u m ( i ) ← n u m ( i − 1 ) num(i) \leftarrow num(i-1) n u m ( i ) ← n u m ( i − 1 )
否则的话,需要开一个新块,n u m ( i ) = t o t + + num(i) = tot++ n u m ( i ) = t o t + +
然后再用 c n t [ n u m ( i ) ] cnt[num(i)] c n t [ n u m ( i ) ] 记录这个块有多少个数 ,直接统计令 c n t [ n u m ( i ) ] + + cnt[num(i)]++ c n t [ n u m ( i ) ] + + 即可
块的数量存储在 c n t [ 0 ⋯ t o t ) cnt[0\cdots tot) c n t [ 0 ⋯ t o t ) 中,实际上中间部分的最大值,就是用 c n t cnt c n t 初始化 rmq \text{rmq} rmq
rmq ( n u m [ l ] + 1 , n u m [ r ] − 1 ) \text{rmq}(num[l]+1, num[r]-1) rmq ( n u m [ l ] + 1 , n u m [ r ] − 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 const int maxn = 1e5 + 10; vector<int> a(maxn, 0); int n, q; vector<int> le(maxn, 0), ri(maxn, 0); vector<int> num(maxn, 0), cnt(maxn, 0); int tot = 0; template<class T> struct RMQ { vector<vector<T> > f; RMQ() = default; RMQ(const vector<T>& a, int n) { int Log = log (n) / log (2) + 1; f.resize(n+1); for (int i = 0; i < n+1; i++) f[i].resize(Log); for (int i = 0; i < n; i++) f[i][0] = a[i]; for (int j = 1; (1<<j) <= n; j ++) { for (int i = 0; i + (1<<j) - 1 < n; i++) { f[i][j ] = max(f[i][j-1], f[i + (1<<(j-1))][j-1]); } } } T qry(int L, int R) const { int k = log (R-L+1) / log (2); while ((1<<(k+1)) <= R-L+1) k++; return max(f[L][k], f[R - (1<<k) + 1][k ]); } }; void pre () { le[0] = 0; for (int i = 1; i < n; i++) { if (a[i] == a[i-1]) le[i] = le[i-1]; else le[i] = i; } ri[n-1] = n-1; for (int i = n-2; i >= 0; i--) { if (a[i] == a[i+1]) ri[i] = ri[i+1]; else ri[i] = i; } tot = 0; for (int i = 0; i < n; i++) { if (i == 0 || (i && a[i] != a[i-1])) num[i] = tot++; else num[i] = num[i-1]; cnt[num[i]]++; } } void dbg () { for (int i = 0; i < n; i++) printf ("%d " , num[i]); printf ("\n" ); } void solve(const RMQ<int> &rmq, int l, int r) { if (num[l] == num[r]) { int res = r - l + 1; printf ("%d\n" , res); return ; } int res1 = ri[l] - l + 1, res2 = r - le[r] + 1; // debug(res1), debug(res2); int res3 = num[l] + 1 <= num[r] - 1 ? rmq.qry(num[l]+1, num[r]-1) : 0; int ans = max({res1, res2, res3}); printf ("%d\n" , ans); } void init () { fill_v(le, 0), fill_v(ri, 0); fill_v(num, 0), fill_v(cnt, 0); } int main () { freopen("input.txt" , "r" , stdin); while (scanf("%d%d" , &n, &q) == 2 && n) { init(); for (int i = 0; i < n; i++) scanf("%d" , &a[i]); // calc le and ri pre(); RMQ<int> rmq(cnt, tot); // dbg(); while (q--) { int l, r; scanf("%d%d" , &l, &r); l--, r--; // rmq(l, r) solve(rmq, l, r); } } }
kruskal 重构树
在处理生成树有关的问题的时候,边和点相关的一些边界条件比较难处理,一种常见的编程技巧是
建新图,将边的问题转化为点带权,如下图所示
都市的泊油路太硬
该问题的关键是快速回答多个查询,对于 [ l , r ] [l, r] [ l , r ] ,需要知道在 MST 中 l → r l \to r l → r 路径上边的最大值
可以如下处理
一开始记录节点个数为 tot \text{tot} tot
在执行 kruskal \text{kruskal} kruskal 的时候,如果 ( u , v ) (u, v) ( u , v ) 不在一个集合中,执行并查集合并
此时 e ( u , v ) e(u, v) e ( u , v ) 是轻量级边 ,将其看作新的节点 + + tot ++\text{tot} + + tot ,给节点赋权值 w ( t o t ) = e ( u , v ) w(tot) = e(u, v) w ( t o t ) = e ( u , v )
p a ( u ) = p a ( v ) = t o t pa(u) = pa(v) = tot p a ( u ) = p a ( v ) = t o t ,将 t o t tot t o t 的两个儿子置为 u , v u, v u , v ,即 G ( t o t , 0 ) = u , G ( t o t , 1 ) = v G(tot, 0) = u, G(tot, 1) = v G ( t o t , 0 ) = u , G ( t o t , 1 ) = v
这样重构树就建完了,值得注意的是,不妨设最后合并的点是 ( u 0 , v 0 ) (u_0, v_0) ( u 0 , v 0 ) ,重构树的根节点 就是 e ( u 0 , v 0 ) = t o t e(u_0, v_0) = tot e ( u 0 , v 0 ) = t o t
kruskal 重构树构造完成之后,要求 maxe ( l , r ) \text{maxe}(l, r) maxe ( l , r ) 瓶颈路或者最长边,就可以转为 dfs \textbf{dfs} dfs 序
方法就是对重构树中序遍历
1 2 3 4 5 6 7 8 9 auto dfs = [&](int x) { if (x == 0) return ; dfs(G[x][0]); dfn[x] = ++clock, seq[dfn[x]] = w[x]; // 这里 w[x] 即边 (G(x, 0), G(x, 1)) 的权值 dfs(G[x][1]); }; dfs(tot);
另外需要注意的是,kruskal \text{kruskal} kruskal 重构树需要的空间为 O ( 2 n ) O(2n) O ( 2 n )
这样,问询 maxe ( x , y ) \text{maxe}(x, y) maxe ( x , y ) 即 x , y x, y x , y 之间路径的最长边权时
可以转换成为 dfs \text{dfs} dfs 序数组 < seq > \left<\text{seq} \right> ⟨ seq ⟩ 中的一段 [ l , r ] [l, r] [ l , r ]
l = dfn [ x ] , r = dfn [ y ] l = \text{dfn}[x], r = \text{dfn}[y] l = dfn [ x ] , r = dfn [ y ] ,可以考虑用 rmq \textbf{rmq} rmq 维护 seq \text{seq} seq ,求最大值
如果是求第 k k k 大点,也可以考虑用主席树等工具
Bond
算法思想是一致的,将最小生成树转为 dfs \text{dfs} dfs 序,具体来说,dfs ( x , f a ) \text{dfs}(x, fa) dfs ( x , f a )
第一次访问 x x x 的时候,令 dfn ( x ) = + + t i m e \text{dfn}(x) = ++time dfn ( x ) = + + t i m e ,同时将点的权值 放入 rmq \text{rmq} rmq 数组 f f f 中
令 f ( dfn ( x ) , 0 ) ← w ( x ) f(\text{dfn}(x), 0) \leftarrow w(x) f ( dfn ( x ) , 0 ) ← w ( x )
然后递归执行 x x x 的分支 y ← G ( x ) , dfs ( y , x ) y \leftarrow G(x), \ \text{dfs}(y, x) y ← G ( x ) , dfs ( y , x ) ,回溯完成后,要接着令 f ( + + t i m e , 0 ) ← w ( x ) f(++time, 0) \leftarrow w(x) f ( + + t i m e , 0 ) ← w ( x )
再递归 x x x 的另一个分支,这样就能保证两个点夹着一个边
编号在 [ n + 1 , 2 n − 1 ] [n+1, 2n-1] [ n + 1 , 2 n − 1 ] 区间的点实际上是边,调用 dfs ( n + 1 , 0 ) \text{dfs}(n+1, 0) dfs ( n + 1 , 0 ) 之后,rmq ( dfn ( l ) , dfn ( r ) ) \text{rmq}(\text{dfn}(l), \text{dfn}(r)) rmq ( dfn ( l ) , dfn ( r ) ) 就是答案
树上倍增处理最小生成树
Bond
最小生成树查询问题,可以使用树上倍增 + LCA 算法 解决
首先求出原图的最小生成树,并根据最小生成树建新图 G G G ,在新图上 dfs \text{dfs} dfs 并且把无根树转为有根树
具体来说,dfs ( x , f a , w ) \text{dfs}(x, fa, w) dfs ( x , f a , w ) ,其中 w w w 表示进入 x x x 这个节点的前向弧的权值 ,在 dfs \text{dfs} dfs 的时候维护以下信息
up ( u , i ) \text{up}(u, i) up ( u , i ) 表示从 u u u 节点往上 2 i 2^{i} 2 i 步能够走到的节点
maxe ( u , i ) \text{maxe}(u, i) maxe ( u , i ) 表示从 u u u 节点往上走 2 i 2^{i} 2 i 个单位所经过的最长边的权值
d ( u ) d(u) d ( u ) 表示 u u u 节点的深度,令 u i ← u p ( u , i − 1 ) ui \leftarrow up(u, i-1) u i ← u p ( u , i − 1 ) ,那么有如下递推关系
up ( u , i ) = up ( u i , i − 1 ) \text{up}(u, i) = \text{up}(ui, i-1) up ( u , i ) = up ( u i , i − 1 ) , maxe ( u , i ) = max ( maxe ( u , i − 1 ) , maxe ( u i , i − 1 ) ) \text{maxe}(u, i) = \max (\text{maxe}(u, i-1), \text{maxe}(ui, i-1)) maxe ( u , i ) = max ( maxe ( u , i − 1 ) , maxe ( u i , i − 1 ) )
初始化 maxe ( x , 0 ) = w , up ( x , 0 ) = f a \text{maxe}(x, 0) = w, \ \text{up}(x, 0) = fa maxe ( x , 0 ) = w , up ( x , 0 ) = f a
处理每一个问询,对于 ( s , t ) (s, t) ( s , t ) ,可以用倍增求 l ← LCA ( s , t ) l \leftarrow \text{LCA}(s, t) l ← LCA ( s , t ) ,那么最终的答案为
max ( path ( l , s ) , path ( l , t ) ) \max (\text{path}(l, s), \text{path}(l, t)) max ( path ( l , s ) , path ( l , t ) ) ,其中 path ( l , s ) \text{path}(l, s) path ( l , s ) 表示 l → s l \to s l → s 路径上的最大值
这个最大值 path ( l , u ) \text{path}(l, u) path ( l , u ) 同样可以用倍增来求
先从大到小遍历深度 i ∈ [ LOG → 0 ] i \in [\text{LOG} \to 0] i ∈ [ LOG → 0 ] ,如果 d ( up ( u , i ) ) ⩾ d ( l ) d(\text{up}(u, i)) \geqslant d(l) d ( up ( u , i ) ) ⩾ d ( l ) ,令 u ← up ( u , i ) u \leftarrow \text{up}(u, i) u ← up ( u , i )
同时更新答案,r e s ← max ( r e s , maxe ( u , i ) ) res \leftarrow \max(res, \text{maxe}(u, i)) r e s ← max ( r e s , maxe ( u , i ) ) ,最后 r e s res r e s 即为所求
朱刘算法