「专题落实」树上问题 - xzz
树的重心与直径的性质
- 一棵树的重心 \(\le2\) 个。
- 树增加新点时,重心可能不动,可能向加点的方向移动 \(1\)。
- 子树内的重心一定在重链上。
- 重心的每一个子树 size 都 \(\le\lfloor\frac{n}{2}\rfloor\)。
- 距离每个点最远的点一定是两个直径端点之一。
- 直径具有可合并性。
关于直径中点的性质,这里 有两道 AtCoder 的题很不错。
例题 1:「CSP-S2019」树的重心
枚举断的边 \((u,v)\)。
考虑怎么求 \(v\) 子树内的重心:
- 由上面的性质 3 可知,我们每次跳重儿子就能找到重心。
- 暴力跳肯定是不现实的,对于优化可以很自然地想到倍增。
- 设 \(f_{i,j}\) 表示从 \(i\) 开始跳 \(2^j\) 次重儿子会跳到哪一个节点。
- 由上面的性质 4 可知,我们跳到最深的 \(size_w\le\lfloor\frac{size_v}{2}\rfloor\) 的点 \(w\) 就是重心。特别的,如果 \(2\times size_w=size_v\),说明重心在 \(w\) 和它的父亲这条边上,此时有两个重心。
子树外的部分大小为 \(n-sz_v\),直接做不太好做,考虑换根:
- 将根从 \(u\) 换到 \(v\) 的时候,如果 \(v\) 是 \(u\) 的重儿子,那么换根后 \(u\) 的重儿子就是次大的儿子,否则还是重儿子。
- 重新更新 \(u\) 的倍增数组和 \(u\)、\(v\) 换根后的子树大小。
- 找到 \(u\) 在新树上的子树重心。
- 不要忘记还原信息。
代码链接:https://loj.ac/s/1380580。
点分治
用于处理树上路径问题。 每次找到树的重心,处理经过重心的所有路径。
注意代码实现中的一大堆细节,不要因为奇怪的原因 TLE,一定要用一条链测试。
点分治应用场景:
- 树上可二分型问题:使用点分治优化一步步走的过程(如树的重心)。
- 路径统计问题:需要统计树上所有路径的某些信息时常用点分治。
- 无换根动态点对信息统计问题:常使用动态点分治。
上面都是蒯的
例题 2:「IOI2011」Race
应该算是模板题了吧。
直接开一个桶表示路径长度是 \(i\) 所需的最短边数。
代码链接:https://paste.ubuntu.com/p/7T7MKfJdwV/。
例题 3:「JOISC 2020 Day4」首都城市
点分治不只能维护路径,维护过分治重心的连通块也是可以的。
强制分治重心一定要选,那么这个颜色的点全部都要选。
如果点 \(x\) 和 \(y\) 都要选,那么 \(x\) 到 \(y\) 的路径上的点也全部都要选。
注意如果有要选的点不在当前子树内可以直接退出,因为这种情况在上一层一定已经算过了。
具体实现就是拿一个队列维护当前要选的点,每次加入父亲。
代码链接:https://loj.ac/s/1382254。
例题 4:「USACO 2018.01 Platinum」Cow at Large
设 \(g_i\) 为 \(i\) 到离点 \(i\) 最近的叶子结点的距离。
则称一个点 \(u\) 为「拦截点」,即 FJ 能在这个节点拦住奶牛,当且仅当 \(dep_u \ge g_u\)。容易发现这些「拦截点」一定都在一些点的子树中,我们只需要记录深度最浅的那个节点即可。即只有 \(dep_u\ge g_u\land dep_{fa_u}<g_{fa_u}\) 的点才会计入贡献。
一个子树的贡献是 \(1\),考虑一步神奇的转换:设 \(deg_i\) 表示点 \(i\) 的度数,那么 \(\sum\limits_{v\in subtree_u}deg_v=2\times size_u-1\),即 \(1=\sum\limits_{v\in subtree_u}(2-deg_v)\)。因此,如果我们令每个点有一个权值 \(a_u=2-deg_u\),则点 \(i\) 的答案 \(ans_i=\sum\limits_{u}[dis(i,u)\ge g_u]a_u\)。
这样我们就把答案变成了点对之间的贡献,点分治即可。
实现上只需要维护一个差分序列,后缀加就变成了单点加,在最后计算一下每个数的贡献即可。
代码链接:https://loj.ac/s/1383415。
虚树
只关心树上的 \(k\) 个点,将树上一些点拿出来,维持原树上的形态建一棵新树。
这个新的树性质很不错:形态与原树相同,且点数不超过 \(2k\)。
求虚树本身非常简单,但套在上面的算法就可能非常恶心(一般需要大讨论和很多繁琐的数据结构),所以建议把根也放进虚树,可以少一些讨论。
例题 5:「SDOI2011」消耗战
模板题,建出虚树后将虚树的边权设为原树上这条链上边权的最小值,树形 DP 即可。
代码链接:https://paste.ubuntu.com/p/B2TqYsVRcF/。
例题 6:「HNOI2014」世界树
首先肯定建出虚树。
可以通过换根 DP 求出虚树上的点被控制的情况。
考虑所有点的位置:虚树上、虚树边上 / 边上点的其它子树里、虚树点的其它子树里。
2 在一条边两点种类不同时比较难,需要二分出分界点然后统计子树中的点数。
3 较简单,减掉虚树中的 \(size\) 即可。
实现的时候考虑一个技巧:先加上当前子树的 \(size\),然后再减去二分出的分界点的子树 \(size\)。
代码链接:https://loj.ac/s/1385686。
DSU on tree
- 先遍历轻儿子,并消除影响。
- 再遍历重儿子,并保留影响。
- 将当前点的贡献计入。
- 将当前点所有轻儿子的贡献计入。
- 如果要消除影响,就进行消除。
- 复杂度大概是 \(\mathcal{O}(n\log n)\)。
Prüfer 序列
一种将有标号无根树与序列一一对应的方法。
构造方法:每次删掉标号最小的叶子,将它连接的点加到序列尾。
该序列长度为 \(n-2\),可以通过该序列得知每个点的度数,进而还原树。
且任给一个序列都可以还原。可知 \(n\) 个点的完全图有 \(n^{n-2}\) 个生成树(Cayley定理)。
Prüfer 序列的推论:有 \(k\) 个连通块,点数分别为 \(s_1 , \dots , s_m\),将它们连成树的方案数为 \(n^{k−2}\prod s_i\)。
扩展 Cayley 定理:有 \(n\) 个有标号点连成 \(s\) 个连通块(且 \(1,\dots, s\) 属于不同连通块),方案数为 \(sn^{n-s-1}\)。如果是有根树,则为 \({n\choose s} sn^{n−s−1}\)。可以枚举点 \(1\) 的度数用归纳法证明。
例题 7:「CF156D」Clues
直接套上面的推论即可。
代码就不放了。。。
例题 8:「THUPC2018」城市地铁规划 / City
由于长度为 \(n-2\) 的任意一个数值在 \([1,n]\) 中的序列与一棵树一一对应,因此只要考虑每个数在 Prüfer 序列中出现的次数即可。
这个可以直接完全背包计算。
代码链接:https://loj.ac/s/1381355。
树上高斯消元
树上每个非叶子节点有一个方程:\(f_u=\sum\limits_{(u,v)\in E}f_vg(u,v)\)。
从下往上将每个节点写成 \(f_u=A_uf_{fa_u}+B_u\) 的形式。
大力代入推式子即可求出 \(A_u\) 和 \(B_u\)。
树哈希
一种比较好的方法是括号序列+字符串哈希。
括号序列是很好的表示树形态的工具,用字符串哈希来维护,只需多记录几个序列长度就可以哈希了。
例题 9:「CF718D」Andrew and Chemistry
参考:https://www.cnblogs.com/werner-yin/p/15893929.html
代码链接:https://codeforces.ml/contest/718/submission/146593809。
杂题 / 套路题选做
例题 10:「Gym103447C」「CCPC2021 Harbin」Colorful Tree
题意:
有一个无色的有根树,你需要把每个叶子节点染成指定的颜色。
每次操作可以将一个子树中的叶子全部染成同一个颜色,求最少的操作数。
\(n \le 10^5\)。
考虑一个朴素 DP:设 \(f_{i,j}\) 表示 \(i\) 的子树已经预染色成 \(j\) 的情况下染成指定颜色的最少操作数。特别的,令 \(f_{i,0}\) 表示没有预染色的情况下最少的操作数。
转移:\(f_{i,j}=\sum\limits_{v\in son_u}\min(f_{v,j},f_{v,0})\),\(f_{i,0}=\min\limits_{1\le j\le n}(f_{i,0},f_{i,j}+1)\)。
第一个方程要与 \(f_{v,0}\) 取 \(\min\) 的原因是如果 \(v\) 子树内没有 \(j\) 颜色,那么情况就相当于没有预染色。
发现不在子树内的颜色没有必要存,所以开一个 map 记录 DP 状态,合并子节点可以直接启发式合并。
合并 map 时候颜色 \(j\) 如果是在小的 map \(A\) 中出现则可以暴力更新;如果不在小的 map \(A\) 出现,但是在大的 map \(B\) 中出现,就需要 \(B[j] \leftarrow B[j] + A[0]\)。
所以需要对 map 附加维护一个懒标记进行全局加法。
具体的,先假设子树内全部选 \(f_{v,0}\),那么 \(tag_u\leftarrow tag_u+tag_v+f_{v,0}\),然后记录 \(\Delta=\min(f_{v,j},f_{v,0})-f_{v,0}\),在转移时加上即可。
代码链接:https://paste.ubuntu.com/p/PQV7wRxW9B/。
例题 11:「CF516D」Drazil and Morning Exercise
首先可以算出 \(f_i\),这个直接从直径的两个端点分别 dfs 就能轻易求出。
看到这种 \(\max-\min\) 的形式就想到双指针。
由 \(f\) 的定义可知,一定存在一个点 \(u\),使得以 \(u\) 为根的情况下 \(f\) 随深度增加而减小,即上小下大。
因此,现将 \(f\) 排序,对于维护连通块,我们按 \(f\) 从大到小枚举点,用并查集维护连通块大小。删点的时候只会将一个点从连通块中分离出去,因此直接在 \(size\) 上 \(-1\) 即可,不会对答案造成影响。
代码链接:https://codeforces.ml/contest/516/submission/146831937。
例题 12:「HNOI2015」开店
题目大意:
给定一颗 \(n(n\leq150000)\) 个点的树,每个点有点权,边有边权(表示两个点之间的距离)。\(q(q\leq200000)\) 次询问,每次询问点权在 \([L, R]\) 之间的所有点到某个点的距离之和。强制在线。
根据 「LNOI2014」LCA 那一题的套路,我们可以将每个点到根的路径全部增加一次覆盖次数,查询的时候直接查这个点到根的路径上所有边的覆盖次数 \(\times\) 边权之和。
看到区间不好处理,可以想想主席树。考虑每次插入一个点,用树剖 + 线段树维护边被覆盖的情况,直接在上一个版本的线段树上进行若干次区间加操作。
查询的时候用 \(rt_r\) 的答案减去 \(rt_{l-1}\) 的答案就是区间 \([l,r]\) 的答案。
代码链接:https://loj.ac/s/1382696。
例题 13:「CF757G」Can Bash Save the Day?
与上一道题很类似。
关键是在于交换操作,容易发现我们只会修改 \(i\) 位置上的线段树,于是交换之后直接重新做一遍链覆盖即可。
注意本题卡空间,需要用到定时重构、调参等卡空间技巧。
代码链接:https://codeforces.ml/contest/757/submission/146635783。

浙公网安备 33010602011771号