参考《算法竞赛进阶指南》 、AcWing题库
树的直径与最近公共祖先
树的直径
给定一棵树, 树中每条边都有一个权值, 树中两点之间的距离定义为连接两点的路径上的边权之和。树中最远的两个节点之间的距离被称为树的直径 , 连接这两点的路径被称为树的最长链 。后者通常也可称为直径, 即直径既是一个数值概念, 也可代指一条 路径。
树的直径一般有两种求法, 时间复杂度都是 O ( N ) O(N) O ( N ) 。我们假设树以 N N N 个点 N − 1 N-1 N − 1 条边的无向图的形式给出, 并存储在邻接表中。
树形 DP 求树的直径
不妨设 1 号节点为根, “ N N N 个点 N − 1 N-1 N − 1 条边的无向图” 就可以看作 “有根树” 。
设 D [ x ] D[x] D [ x ] 表示从节点 x x x 出发走向以 x x x 为根的子树, 能够到达的最远节点的距离。设 x x x 的子节点为 y 1 , y 2 , ⋯ , y t y_{1}, y_{2}, \cdots, y_{t} y 1 , y 2 , ⋯ , y t , e d g e ( x , y ) edge(x, y) e d g e ( x , y ) 表示边权, 显然有:
D [ x ] = max 1 ≤ i ≤ t { D [ y i ] + edge ( x , y i ) } D[x]=\max _{1 \leq i \leq t}\left\{D\left[y_{i}\right]+\operatorname{edge}\left(x, y_{i}\right)\right\}
D [ x ] = 1 ≤ i ≤ t max { D [ y i ] + edge ( x , y i ) }
接下来, 我们可以考虑对每个节点 x x x 求出 “经过节点 x x x 的最长链的长度” F [ x ] F[x] F [ x ] , 整棵树的直径就是 max 1 ≤ x ≤ n { F [ x ] } \max _{1 \leq x \leq n}\{F[x]\} max 1 ≤ x ≤ n { F [ x ]} 。
那么如何求出 F [ x ] F[x] F [ x ] 呢? 对于 x x x 的任意两个节点 y i y_{i} y i 和 y j y_{j} y j , “经过节点 x x x 的最长链的长度” 可以通过四个部分构成: 从 y i y_{i} y i 到 y i y_{i} y i 子树中的最远距离, 边 ( x , y i ) \left(x, y_{i}\right) ( x , y i ) , 边 ( x , y j ) \left(x, y_{j}\right) ( x , y j ) , 从 y j y_{j} y j 到 y j y_{j} y j 子树中的最远距离。不妨设 j < i j<i j < i , 因此:
F [ x ] = max 1 ≤ j < i ≤ t { D [ y i ] + D [ y j ] + edge ( x , y i ) + edge ( x , y j ) } F[x]=\max _{1 \leq j<i \leq t}\left\{D\left[y_{i}\right]+D\left[y_{j}\right]+\operatorname{edge}\left(x, y_{i}\right)+\operatorname{edge}\left(x, y_{j}\right)\right\}
F [ x ] = 1 ≤ j < i ≤ t max { D [ y i ] + D [ y j ] + edge ( x , y i ) + edge ( x , y j ) }
我们没有必要使用两层循环来枚举 i , j i, j i , j 。请读者思考计算 D [ x ] D[x] D [ x ] 的过程。在子节点的循环将要枚举到 i i i 时, D [ x ] D[x] D [ x ] 恰好就保存了从节点 x x x 出发走向 “以 y j ( j < i ) y_{j}(j<i) y j ( j < i ) 为根的子树”, 能够到达的最远节点的距离, 这个距离就是 max 1 ≤ j < i { D [ y j ] + edge ( x , y j ) } \max _{1 \leq j<i}\left\{D\left[y_{j}\right]+\operatorname{edge}\left(x, y_{j}\right)\right\} max 1 ≤ j < i { D [ y j ] + edge ( x , y j ) } 。 所以, 此时我们先用 D [ x ] + D [ y i ] + edge ( x , y i ) D[x]+D\left[y_{i}\right]+\operatorname{edge}\left(x, y_{i}\right) D [ x ] + D [ y i ] + edge ( x , y i ) 更新 F [ x ] F[x] F [ x ] , 再用 D [ y i ] + edge ( x , y i ) D\left[y_{i}\right]+\operatorname{edge}\left(x, y_{i}\right) D [ y i ] + edge ( x , y i ) 更新 D [ x ] D[x] D [ x ] 即可。
1 2 3 4 5 6 7 8 9 10 11 void dp (int x) { v[x] = 1 ; for (int i = head[x]; i; i = Next[i]) { int y = ver[i]; if (v[y]) continue ; dp (y); ans = max (ans, d[x] + d[y] + edge[i]); d[x] = max (d[x], d[y] + edge[i]); } }
两次 BFS 求树的直径
通过两次 BFS 或者两次 DFS 也可以求出树的直径, 并且更容易计算出直径上的具体节点。详细地说, 这种做法包括两步:
从任意一个节点出发, 通过 BFS 或 DFS 对树进行一次遍历, 求出与出发点距离最远的节点, 记为 p p p 。
从节点 p p p 出发, 通过 BFS 或 DFS 再进行一次遍历, 求出与 p p p 距离最远的节点, 记为 q q q 。
从 p p p 到 q q q 的路径就是树的一条直径。这是因为 p p p 一定是直径的一端, 否则总能找到一条更长的链, 与直径的定义矛盾。请读者尝试详细证明这个结论, 此处就不再赘述。既然 p p p 是直径的一端, 那么与 p p p 距离最远的 q q q 当然就是直径的另一端了。在第 2 步的遍历过程中, 可以记录下来每个点第一次被访问时的前驱节点。最后从 q q q 递归回到 p p p , 即可得到直径的具体方案。
树的直径可以作为很多树上问题的突破口。接下来我们给出两道例题。除此之外, 读者还将在图论总结与练习中解决树的直径的必须边等问题。
在一个地区有 n n n 个村庄,编号为 1 , 2 , … , n 1,2,…,n 1 , 2 , … , n 。
有 n − 1 n-1 n − 1 条道路连接着这些村庄,每条道路刚好连接两个村庄,从任何一个村庄,都可以通过这些道路到达其他任一个村庄。
每条道路的长度均为 1 1 1 个单位。
为保证该地区的安全,巡警车每天都要到所有的道路上巡逻。
警察局设在编号为 1 1 1 的村庄里,每天巡警车总是从警局出发,最终又回到警局。
为了减少总的巡逻距离,该地区准备在这些村庄之间建立 K K K 条新的道路,每条新道路可以连接任意两个村庄。
两条新道路可以在同一个村庄会合或结束,甚至新道路可以是一个环。
因为资金有限,所以 K K K 只能为 1 1 1 或 2 2 2 。
同时,为了不浪费资金,每天巡警车必须经过新建的道路正好一次。
编写一个程序,在给定村庄间道路信息和需要新建的道路数的情况下,计算出最佳的新建道路的方案,使得总的巡逻距离最小。
输入格式
第一行包含两个整数 n n n 和 K K K 。
接下来 n − 1 n-1 n − 1 行每行两个整数 a a a 和 b b b ,表示村庄 a a a 和 b b b 之间有一条道路。
输出格式
输出一个整数,表示新建了 K K K 条道路后能达到的最小巡逻距离。
数据范围
3 ≤ n ≤ 100000 3 \le n \le 100000 3 ≤ n ≤ 100000 ,
1 ≤ K ≤ 2 1 \le K \le 2 1 ≤ K ≤ 2 ,
1 ≤ a , b ≤ n 1 \le a,b \le n 1 ≤ a , b ≤ n
输入样例:
1 2 3 4 5 6 7 8 8 1 1 2 3 1 3 4 5 3 7 5 8 5 5 6
输出样例:
算法分析
Solution
设 T = ( V , E , W ) T=(V, E, W) T = ( V , E , W ) 是一个无圈且连通的无向图(也称为无根树),每条边带有正整数的权,我们称 T T T 为树网(treenetwork),其中 V , E V, E V , E 分别表示结点与边的集合,W W W 表示各边长度的集合,并设 T T T 有 n n n 个结点。
路径:树网中任何两结点 a , b a,b a , b 都存在唯一的一条简单路径,用 d ( a , b ) d(a,b) d ( a , b ) 表示以 a , b a,b a , b 为端点的路径的长度,它是该路径上各边长度之和。
我们称 d ( a , b ) d(a,b) d ( a , b ) 为 a , b a,b a , b 两结点间的距离。
一点 v v v 到一条路径 P P P 的距离为该点与 P P P 上的最近的结点的距离:
d ( v , P ) = m i n { d ( v , u ) } d(v,P)=min\{d(v,u)\} d ( v , P ) = min { d ( v , u )} ,u u u 为路径 P P P 上的结点。
树网的直径:树网中最长的路径称为树网的直径。
对于给定的树网 T T T ,直径不一定是唯一的,但可以证明:各直径的中点(不一定恰好是某个结点,可能在某条边的内部)是唯一的,我们称该点为树网的中心。
偏心距 E C C ( F ) ECC(F) ECC ( F ) :树网 T T T 中距路径 F F F 最远的结点到路径 F F F 的距离,即:
E C C ( F ) = m a x { d ( v , F ) , v ∈ V } ECC(F)=max\{d(v,F),v∈V\} ECC ( F ) = ma x { d ( v , F ) , v ∈ V }
任务:对于给定的树网 T = ( V , E , W ) T=(V, E,W) T = ( V , E , W ) 和非负整数 s s s ,求一个路径 F F F ,它是某直径上的一段路径(该路径两端均为树网中的结点),其长度不超过 s s s (可以等于 s s s ),使偏心距 E C C ( F ) ECC(F) ECC ( F ) 最小。
我们称这个路径为树网 T = ( V , E , W ) T=(V,E,W) T = ( V , E , W ) 的核(Core)。
必要时,F F F 可以退化为某个结点。
一般来说,在上述定义下,核不一定只有一个,但最小偏心距是唯一的。
输入格式
包含 n n n 行: 第 1 1 1 行,两个正整数 n n n 和 s s s ,中间用一个空格隔开,其中 n n n 为树网结点的个数,s s s 为树网的核的长度的上界,设结点编号依次为 1 , 2 , … , n 1, 2, …, n 1 , 2 , … , n 。
从第 2 2 2 行到第 n n n 行,每行给出 3 3 3 个用空格隔开的正整数,依次表示每一条边的两个端点编号和长度。
例如,2 4 7
表示连接结点 2 2 2 与 4 4 4 的边的长度为 7 7 7 。
所给的数据都是正确的,不必检验。
输出格式
只有一个非负整数,为指定意义下的最小偏心距。
数据范围
n ≤ 500000 , s < 2 31 n \le 500000,s < 2^{31} n ≤ 500000 , s < 2 31
输入样例:
1 2 3 4 5 5 2 1 2 5 2 3 2 2 4 4 2 5 3
输出样例:
算法分析
Solution
最近公共祖先(LCA)
给定一棵有根树, 若节点 z z z 既是节点 x x x 的祖先, 也是节点 y y y 的祖先, 则称 z z z 是 x , y x, y x , y 的公共祖先。在 x , y x, y x , y 的所有公共祖先中, 深度最大的一个称为 x , y x, y x , y 的最近公共祖先, 记为 LCA ( x , y ) (x, y) ( x , y ) 。
LCA ( x , y ) \operatorname{LCA}(x, y) LCA ( x , y ) 是 x x x 到根的路径与 y y y 到根的路径的交会点。它也是 x x x 与 y y y 之间的路径上深度最小的节点。求最近公共祖先的方法通常有三种:
向上标记法
从 x x x 向上走到根节点, 并标记所有经过的节点。
从 y y y 向上走到根节点, 当第一次遇到己标记的节点时, 就找到了 LCA ( x , y ) \operatorname{LCA}(x, y) LCA ( x , y ) 。
对于每个询问, 向上标记法的时间复杂度最坏为 O ( n ) O(n) O ( n ) 。
树上倍增法
树上倍增法是一个很重要的算法。除了求 LCA 之外, 它在很多问题中都有广泛应用。设 F [ x , k ] F[x, k] F [ x , k ] 表示 x x x 的 2 k 2^{k} 2 k 辈祖先, 即从 x x x 向根节点走 2 k 2^{k} 2 k 步到达的节点。特别地, 若该节点不存在, 则令 F [ x , k ] = 0 F[x, k]=0 F [ x , k ] = 0 。F [ x , 0 ] F[x, 0] F [ x , 0 ] 就是 x x x 的父节点。除此之外, ∀ k ∈ [ 1 , log n ] \forall k \in [1, \log n] ∀ k ∈ [ 1 , log n ] , F [ x , k ] = F [ F [ x , k − 1 ] , k − 1 ] F[x, k]=F[F[x, k-1], k-1] F [ x , k ] = F [ F [ x , k − 1 ] , k − 1 ] 。
这类似于一个动态规划的过程, “阶段” 就是节点的深度。因此, 我们可以对树进行广度优先遍历, 按照层次顺序, 在节点入队之前, 计算它在 F F F 数组中相应的值。
以上部分是预处理, 时间复杂度为 O ( n log n ) O(n \log n) O ( n log n ) , 之后可以多次对不同的 x , y x, y x , y 计算 LCA, 每次询问的时间复杂度为 O ( log n ) O(\log n) O ( log n ) 。
基于 F F F 数组计算 LCA ( x , y ) \operatorname{LCA}(x, y) LCA ( x , y ) 分为以下几步:
设 d [ x ] d[x] d [ x ] 表示 x x x 的深度。不妨设 d [ x ] ≥ d [ y ] d[x] \geq d[y] d [ x ] ≥ d [ y ] (否则可交换 x , y x, y x , y )。
用二进制拆分思想, 把 x x x 向上调整到与 y y y 同一深度。
具体来说, 就是依次尝试从 x x x 向上走 k = 2 log n , ⋯ , 2 1 , 2 0 k=2^{\log n}, \cdots, 2^{1}, 2^{0} k = 2 l o g n , ⋯ , 2 1 , 2 0 步, 检查到达的节点是否比 y y y 深。在每次检查中, 若是, 则令 x = F [ x , k ] x=F[x, k] x = F [ x , k ] 。
若此时 x = y x=y x = y , 说明已经找到了 LCA, LCA 就等于 y y y 。这就是上面的图中的第三种情况。
用二进制拆分思想, 把 x , y x, y x , y 同时向上调整, 并保持深度一致且二者不相会。 具体来说, 就是依次尝试把 x , y x, y x , y 同时向上走 k = 2 log n , ⋯ , 2 1 , 2 0 k=2^{\log n}, \cdots, 2^{1}, 2^{0} k = 2 l o g n , ⋯ , 2 1 , 2 0 步, 在每次尝试 中, 若 F [ x , k ] ≠ F [ y , k ] F[x, k] \neq F[y, k] F [ x , k ] = F [ y , k ] (即仍末相会), 则令 x = F [ x , k ] , y = F [ y , k ] x=F[x, k], y=F[y, k] x = F [ x , k ] , y = F [ y , k ] 。
此时 x , y x, y x , y 必定只差一步就相会了, 它们的父节点 F [ x , 0 ] F[x, 0] F [ x , 0 ] 就是 LCA。
请读者画图理解, 并参阅下面的程序。程序以模板题 HDOJ2586 “How far away?” 为例, 多次查询树上两点之间的距离, 时间复杂度为 O ( ( n + m ) log n ) \mathrm{O}((n+m) \log n) O (( n + m ) log 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 64 65 66 67 68 69 70 #include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <queue> #include <cmath> using namespace std;const int SIZE = 50010 ;int f[SIZE][20 ], d[SIZE], dist[SIZE];int ver[2 * SIZE], Next[2 * SIZE], edge[2 * SIZE], head[SIZE];int T, n, m, tot, t;queue<int > q; void add (int x, int y, int z) { ver[++tot] = y; edge[tot] = z; Next[tot] = head[x]; head[x] = tot; } void bfs () { q.push (1 ); d[1 ] = 1 ; while (q.size ()) { int x = q.front (); q.pop (); for (int i = head[x]; i; i = Next[i]) { int y = ver[i]; if (d[y]) continue ; d[y] = d[x] + 1 ; dist[y] = dist[x] + edge[i]; f[y][0 ] = x; for (int j = 1 ; j <= t; j++) f[y][j] = f[f[y][j - 1 ]][j - 1 ]; q.push (y); } } } int lca (int x, int y) { if (d[x] > d[y]) swap (x, y); for (int i = t; i >= 0 ; i--) if (d[f[y][i]] >= d[x]) y = f[y][i]; if (x == y) return x; for (int i = t; i >= 0 ; i--) if (f[x][i] != f[y][i]) x = f[x][i], y = f[y][i]; return f[x][0 ]; } int main () { cin >> T; while (T--) { cin >> n >> m; t = (int )(log (n) / log (2 )) + 1 ; for (int i = 1 ; i <= n; i++) head[i] = d[i] = 0 ; tot = 0 ; for (int i = 1 ; i < n; i++) { int x, y, z; scanf ("%d%d%d" , &x, &y, &z); add (x, y, z), add (y, x, z); } bfs (); for (int i = 1 ; i <= m; i++) { int x, y; scanf ("%d%d" , &x, &y); printf ("%d\n" , dist[x] + dist[y] - 2 * dist[lca (x, y)]); } } }
LCA 的 Tarjan 算法
Tarjan 算法本质上是使用并查集对 “向上标记法” 的优化。它是一个离线算法, 需要把 m m m 个询问一次性读入, 统一计算, 最后统一输出。时间复杂度为 O ( n + m ) \mathrm{O}(n+m) O ( n + m ) 。
在深度优先遍历的任意时刻, 树中节点分为三类:
已经访问完毕并且回溯的节点。在这些节点上标记一个整数 2。
已经开始递归, 但尚未回溯的节点。这些节点就是当前正在访问的节点 x x x 以及 x x x 的祖先。在这些节点上标记一个整数 1 。
尚未访问的节点。这些节点没有标记。
对于正在访问的节点 x x x , 它到根节点的路径已经标记为 1 。若 y y y 是已经访问完毕并且回溯的节点, 则 LCA ( x , y ) \operatorname{LCA}(x, y) LCA ( x , y ) 就是从 y y y 向上走到根, 第一个遇到的标记为 1 的节点。
可以利用并查集进行优化, 当一个节点获得整数 2 的标记时, 把它所在的集合合并到它的父节点所在的集合中(合并时它的父节点标记一定为 1, 且单独构成一个集合)。
这相当于每个完成回溯的节点都有一个指针指向它的父节点, 只需查询 y y y 所在集合的代表元素 (并查集的 get 操作), 就等价于从 y y y 向上一直走到一个开始递归但尚未回溯的节点 (具有标记 1), 即 LCA ( x , y ) \operatorname{LCA}(x, y) LCA ( x , y ) 。
在 x x x 回溯之前, 标记情况与合并情况如下图所示。黑色表示标记为 1 , 灰色表示标记为 2 , 白色表示没有标记, 箭头表示执行了合并操作。
此时扫描与 x x x 相关的所有询问, 若询问当中的另一个点 y y y 的标记为 2 , 就知道了该询问的回答应该是 y y y 在并查集中的代表元素 (并查集中 get(y)函数的结果)。
下面的参考程序以模板题 HDOJ2586 “How far away?” 为例, 多次查询树上两点之间的距离, 时间复杂度为 O ( n + m ) O(n+m) 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 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 #include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <vector> using namespace std;const int SIZE = 50010 ;int ver[2 * SIZE], Next[2 * SIZE], edge[2 * SIZE], head[SIZE];int fa[SIZE], d[SIZE], v[SIZE], lca[SIZE], ans[SIZE];vector<int > query[SIZE], query_id[SIZE]; int T, n, m, tot, t;void add (int x, int y, int z) { ver[++tot] = y; edge[tot] = z; Next[tot] = head[x]; head[x] = tot; } void add_query (int x, int y, int id) { query[x].push_back (y), query_id[x].push_back (id); query[y].push_back (x), query_id[y].push_back (id); } int get (int x) { if (x == fa[x]) return x; return fa[x] = get (fa[x]); } void tarjan (int x) { v[x] = 1 ; for (int i = head[x]; i; i = Next[i]) { int y = ver[i]; if (v[y]) continue ; d[y] = d[x] + edge[i]; tarjan (y); fa[y] = x; } for (int i = 0 ; i < query[x].size (); i++) { int y = query[x][i]; int id = query_id[x][i]; if (v[y] == 2 ) { int lca = get (y); ans[id] = min (ans[id], d[x] + d[y] - 2 * d[lca]); } } v[x] = 2 ; } int main () { cin >> T; while (T--) { cin >> n >> m; for (int i = 1 ; i <= n; i++) { head[i] = 0 ; query[i].clear (), query_id[i].clear (); fa[i] = i, v[i] = 0 ; } tot = 0 ; for (int i = 1 ; i < n; i++) { int x, y, z; scanf ("%d%d%d" , &x, &y, &z); add (x, y, z), add (y, x, z); } for (int i = 1 ; i <= m; i++) { int x, y; scanf ("%d%d" , &x, &y); if (x == y) ans[i] = 0 ; else { add_query (x, y, i); ans[i] = 1 << 30 ; } } tarjan (1 ); for (int i = 1 ; i <= m; i++) printf ("%d\n" , ans[i]); } }
树上差分
在 “前缀和与差分” 中, 我们定义了一个序列的前缀和与差分序列, 并通过差分技巧, 把 “区间” 的增减转化为 “左端点加 1 , 右端点减 1 ”。根据 “差分序列的前缀和是原序列” 这一原理, 在树上可以进行类似的简化, 其中 “区间操作” 对应为 “路径操作”, “前缀和” 对应为 “子树和”。我们通过具体的例子来详细探讨。
传说中的暗之连锁被人们称为 Dark。
Dark 是人类内心的黑暗的产物,古今中外的勇者们都试图打倒它。
经过研究,你发现 Dark 呈现无向图的结构,图中有 N N N 个节点和两类边,一类边被称为主要边,而另一类被称为附加边。
Dark 有 N – 1 N – 1 N –1 条主要边,并且 Dark 的任意两个节点之间都存在一条只由主要边构成的路径。
另外,Dark 还有 M M M 条附加边。
你的任务是把 Dark 斩为不连通的两部分。
一开始 Dark 的附加边都处于无敌状态,你只能选择一条主要边切断。
一旦你切断了一条主要边,Dark 就会进入防御模式,主要边会变为无敌的而附加边可以被切断。
但是你的能力只能再切断 Dark 的一条附加边。
现在你想要知道,一共有多少种方案可以击败 Dark。
注意,就算你第一步切断主要边之后就已经把 Dark 斩为两截,你也需要切断一条附加边才算击败了 Dark。
输入格式
第一行包含两个整数 N N N 和 M M M 。
之后 N – 1 N – 1 N –1 行,每行包括两个整数 A A A 和 B B B ,表示 A A A 和 B B B 之间有一条主要边。
之后 M M M 行以同样的格式给出附加边。
输出格式
输出一个整数表示答案。
数据范围
N ≤ 100000 , M ≤ 200000 N \le 100000,M \le 200000 N ≤ 100000 , M ≤ 200000 ,数据保证答案不超过2 31 − 1 2^{31}-1 2 31 − 1
输入样例:
输出样例:
算法分析
Solution
深绘里一直很讨厌雨天。
灼热的天气穿透了前半个夏天,后来一场大雨和随之而来的洪水,浇灭了一切。
虽然深绘里家乡的小村落对洪水有着顽固的抵抗力,但也倒了几座老房子,几棵老树被连根拔起,以及田地里的粮食被弄得一片狼藉。
无奈的深绘里和村民们只好等待救济粮来维生。
不过救济粮的发放方式很特别。
有 n n n 个点,形成一个树状结构。
有 m m m 次发放操作,每次选择两个点 x , y x,y x , y ,对 x x x 到 y y y 的路径上(包括 x , y x,y x , y )的每个点发放一袋 z z z 类型的物品。
求完成所有发放操作后,每个点存放最多的是哪种类型的物品。
输入格式
第一行两个正整数 n , m n,m n , m ,含义如题目所示。
接下来 n − 1 n-1 n − 1 行,每行两个数 ( a , b ) (a,b) ( a , b ) ,表示 ( a , b ) (a,b) ( a , b ) 间有一条边。
再接下来 m m m 行,每行三个数 ( x , y , z ) (x,y,z) ( x , y , z ) ,含义如题目所示。
输出格式
共 n n n 行,第 i i i 行一个整数,表示第 i i i 座房屋里存放的最多的是哪种救济粮,如果有多种救济粮存放次数一样,输出编号最小的。
如果某座房屋里没有救济粮,则对应一行输出 0 0 0 。
数据范围
1 ≤ n , m ≤ 100000 1 \le n,m \le 100000 1 ≤ n , m ≤ 100000 ,
1 ≤ z ≤ 1 0 5 1 \le z \le 10^5 1 ≤ z ≤ 1 0 5
输入样例:
1 2 3 4 5 6 7 8 5 3 1 2 3 1 3 4 5 3 2 3 3 1 5 2 3 3 3
输出样例:
算法分析
Solution
小 C C C 同学认为跑步非常有趣,于是决定制作一款叫作《天天爱跑步》的游戏。
《天天爱跑步》是一个养成类游戏,需要玩家每天按时上线,完成打卡任务。
这个游戏的地图可以看作一棵包含 n n n 个节点和 n − 1 n-1 n − 1 条边的树,任意两个节点存在一条路径互相可达。
树上节点的编号是 1 ∼ n 1 \sim n 1 ∼ n 之间的连续正整数。
现在有 m m m 个玩家,第 i i i 个玩家的起点为 S i S_i S i ,终点为 T i T_i T i 。
每天打卡任务开始时,所有玩家在第 0 0 0 秒同时从自己的起点出发,以每秒跑一条边的速度,不间断地沿着最短路径向着自己的终点跑去,跑到终点后该玩家就算完成了打卡任务。
因为地图是一棵树,所以每个人的路径是唯一的。
小 C C C 想知道游戏的活跃度,所以在每个节点上都放置了一个观察员。
在节点 j j j 的观察员会选择在第 W j W_j W j 秒观察玩家,一个玩家能被这个观察员观察到当且仅当该玩家在第 W j W_j W j 秒也正好到达了节点 j j j 。
小 C C C 想知道每个观察员会观察到多少人?
注意:我们认为一个玩家到达自己的终点后,该玩家就会结束游戏,他不能等待一段时间后再被观察员观察到。
即对于把节点 j j j 作为终点的玩家:若他在第 W j W_j W j 秒前到达终点,则在节点 j j j 的观察员不能观察到该玩家;若他正好在第 W j W_j W j 秒到达终点,则在节点 j j j 的观察员可以观察到这个玩家。
输入格式
第一行有两个整数 n n n 和 m m m 。
其中 n n n 代表树的结点数量,同时也是观察员的数量,m m m 代表玩家的数量。
接下来 n − 1 n-1 n − 1 行每行两个整数 U U U 和 V V V ,表示结点 U U U 到结点 V V V 有一条边。
接下来一行 n n n 个整数,其中第个整数为 W j W_j W j ,表示结点出现观察员的时间。
接下来 m m m 行,每行两个整数 S i S_i S i 和 T i T_i T i ,表示一个玩家的起点和终点。
输出格式
一行 n n n 个整数,第 i i i 个整数表示结点 i i i 的观察员可以观察到多少人。
数据范围
1 ≤ n , m ≤ 3 × 1 0 5 1 \le n,m \le 3 \times 10^5 1 ≤ n , m ≤ 3 × 1 0 5
输入样例:
1 2 3 4 5 6 7 8 9 10 6 3 2 3 1 2 1 4 4 5 4 6 0 2 5 1 2 3 1 5 1 3 2 6
输出样例:
算法分析
Solution
LCA 的综合应用
Adera 是 Microsoft 应用商店中的一款解谜游戏。
异象石是进入 Adera 中异时空的引导物,在 Adera 的异时空中有一张地图。
这张地图上有 N N N 个点,有 N − 1 N-1 N − 1 条双向边把它们连通起来。
起初地图上没有任何异象石,在接下来的 M M M 个时刻中,每个时刻会发生以下三种类型的事件之一:
地图的某个点上出现了异象石(已经出现的不会再次出现);
地图某个点上的异象石被摧毁(不会摧毁没有异象石的点);
向玩家询问使所有异象石所在的点连通的边集的总长度最小是多少。
请你作为玩家回答这些问题。
输入格式
第一行有一个整数 N N N ,表示点的个数。
接下来 N − 1 N-1 N − 1 行每行三个整数 x , y , z x,y,z x , y , z ,表示点 x x x 和 y y y 之间有一条长度为 z z z 的双向边。
第 N + 1 N+1 N + 1 行有一个正整数 M M M 。
接下来 M M M 行每行是一个事件,事件是以下三种格式之一:
+ x
表示点 x x x 上出现了异象石
- x
表示点 x x x 上的异象石被摧毁
?
表示询问使当前所有异象石所在的点连通所需的边集的总长度最小是多少。
输出格式
对于每个 ?
事件,输出一个整数表示答案。
数据范围
1 ≤ N , M ≤ 1 0 5 1 \le N,M \le 10^5 1 ≤ N , M ≤ 1 0 5 ,
1 ≤ x , y ≤ N 1 \le x,y \le N 1 ≤ x , y ≤ N ,
x ≠ y x \neq y x = y ,
1 ≤ z ≤ 1 0 9 1 \le z \le 10^9 1 ≤ z ≤ 1 0 9
输入样例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 6 1 2 1 1 3 5 4 1 7 4 5 3 6 4 2 10 + 3 + 1 ? + 6 ? + 5 ? - 6 - 3 ?
输出样例:
算法分析
Solution
给定一张 N N N 个点 M M M 条边的无向图,求无向图的严格次小生成树。
设最小生成树的边权之和为 s u m sum s u m ,严格次小生成树就是指边权之和大于 s u m sum s u m 的生成树中最小的一个。
输入格式
第一行包含两个整数 N N N 和 M M M 。
接下来 M M M 行,每行包含三个整数 x , y , z x,y,z x , y , z ,表示点 x x x 和点 y y y 之前存在一条边,边的权值为 z z z 。
输出格式
包含一行,仅一个数,表示严格次小生成树的边权和。(数据保证必定存在严格次小生成树)
数据范围
N ≤ 1 0 5 , M ≤ 3 × 1 0 5 N \le 10^5,M \le 3 \times 10^5 N ≤ 1 0 5 , M ≤ 3 × 1 0 5
输入样例:
1 2 3 4 5 6 7 5 6 1 2 1 1 3 2 2 4 3 3 5 4 3 4 3 4 5 6
输出样例:
算法分析
Solution
H H H 国有 n n n 个城市,这 n n n 个城市用 n − 1 n-1 n − 1 条双向道路相互连通构成一棵树,1 1 1 号城市是首都,也是树中的根节点。
H H H 国的首都爆发了一种危害性极高的传染病。
当局为了控制疫情,不让疫情扩散到边境城市(叶子节点所表示的城市),决定动用军队在一些城市建立检查点,使得从首都到边境城市的每一条路径上都至少有一个检查点,边境城市也可以建立检查点。
但要注意的是,首都是不能建立检查点的。
现在,在 H H H 国的一些城市中已经驻扎有军队,且一个城市可以驻扎多个军队。
军队总数为 m m m 支。
一支军队可以在有道路连接的城市间移动,并在除首都以外的任意一个城市建立检查点,且只能在一个城市建立检查点。
一支军队经过一条道路从一个城市移动到另一个城市所需要的时间等于道路的长度(单位:小时)。
请问:最少需要多少个小时才能控制疫情?
注意:不同的军队可以同时移动。
输入格式
第一行一个整数 n n n ,表示城市个数。
接下来的 n − 1 n-1 n − 1 行,每行 3 3 3 个整数,u 、 v 、 w u、v、w u 、 v 、 w ,每两个整数之间用一个空格隔开,表示从城市 u u u 到城市 v v v 有一条长为 w w w 的道路,数据保证输入的是一棵树,且根节点编号为 1 1 1 。
接下来一行一个整数 m m m ,表示军队个数。
接下来一行 m m m 个整数,每两个整数之间用一个空格隔开,分别表示这 m m m 个军队所驻扎的城市的编号。
输出格式
共一行,包含一个整数,表示控制疫情所需要的最少时间。如果无法控制疫情则输出 − 1 -1 − 1 。
数据范围
2 ≤ m ≤ n ≤ 50000 2 \le m \le n \le 50000 2 ≤ m ≤ n ≤ 50000 ,
0 < w < 1 0 9 0 < w < 10^9 0 < w < 1 0 9
输入样例:
1 2 3 4 5 6 4 1 2 1 1 3 2 3 4 3 2 2 2
输出样例:
算法分析
Solution