虚树小记
command_block
2021-03-19 22:01:39
> 虚树是处理树上点集问题的有力而直观的工具。
**提示** : 鉴于初次学习虚树的同学树论经验一般不丰富,建议初学者在一周目时不必同步关注形式化证明,优先感性理解虚树的形式以及核心算法。
$\color{blue}\Large\star$ 标记代表关键性质, $\color{red}\Large\star$ 标记代表扩展内容。
------------
## 定义
- 给定一棵无根树 $T$ ,定义点集 $S$ 的虚树(Virtual Tree)为 : 最小的,包含 $S$ 的 $T$ 的连通子图。
不难发现,虚树仍然是树。下面是两个虚树的例子,其中蓝色点为点集 $S$ ,带有红色边框的部分为虚树。
![](https://cdn.luogu.com.cn/upload/image_hosting/x5ob8k6f.png?x-oss-process=image/resize,m_lfit,w_420)
在处理与点集 $S$ 有关的问题时,我们希望复杂度是和 $O(|S|)$ 有关,而不是和整棵树的大小 $O(|T|)$ 有关。
但根据虚树的定义,较小的 $S$ 可能产生很大的虚树。(如 $T$ 为一条链, $S$ 为链的两个端点,此时虚树为整个 $T$ )
所以,直接在未简化的虚树上统计并不能很好地优化复杂度。
考虑进一步挖掘虚树的性质,将其结构简化,使得信息量(需要处理的对象个数)降低到 $O(|S|)$ 级别。
## 经典性质
不妨选定节点 $1$ 为根,将树 $T$ 变为有根树,点集的虚树也随之变为有根树。
- $\color{blue}\Large\star$ **性质①** 虚树中,儿子数 $\geq 2$ 的点的数目 $\leq |S|-1$ 。
**证明** : 在虚树上进行 `dfs` 。
称 $S$ 中的点为“关键点”。记 $D_u$ 为节点 $u$ 子树中的关键点集合。显然,所有在虚树中的 $u$ 的 $D_u$ 均不为空。
称儿子数 $\geq 2$ 的点为“分叉点”。
某个分叉点有至少两个子树,该点的 $D$ 集合相当于将子树的 $D$ 集合合并。(若自己是关键点,也会将自己并入)
总共合并的次数不多于 $|S|-1$ 次,故分叉点的个数也 $\leq |S|-1$。
这等价地意味着,虚树中无分叉链的条数 $\leq 2|S|-1$。(考虑所有关键点及分叉点的父边)
如图,$|S|=3$ 的点集产生的 $4$ 条无分叉链 :
![](https://cdn.luogu.com.cn/upload/image_hosting/3m0rjld4.png)
- $\color{red}\Large\star$ **应用例**
动态开点 Trie 可以看做是维护了一棵完整的 Trie 的虚树。
于是,插入 $k$ 次的动态开点 Trie 的分叉点个数和无分叉链个数均为 $O(k)$。
该结论的一些应用 :
- 证明后缀树的状态数是 $O(k)$ 的。
- 设计压缩 01Trie 。例 : [EA : 题解 P6136 【【模板】普通平衡树(数据加强版)】](https://www.luogu.com.cn/blog/EternalAlexander/solution-p6136)
- 设计空间复杂度 $O(n)$ 的压缩线段树合并。
- **例题** : PKUSC2019 D1T3
**题意** : 给定长为 $n$ 的数组 $A$ 以及 $m$ 个询问 $(x_i,y_i)$ ,每次询问**是否**存在一个 $k$ 使得 $a_{x_i}$ 异或 $k$ 在所有数异或 $k$ 中排第 $y_i$ 。
$n\leq 5\times 10^4,m\leq 10^6,a_i\leq 2^{60}$。
首先用 $A$ 中的所有数建立 01Trie 。若 $k$ 的某一位为 $1$ ,则相当于将该层的所有点的左右儿子交换。
考虑如何处理单组询问。将 $A_x$ 祖先路径的分叉都找出来,可以通过调整 $k$ 使得某个分叉内的所有数小于或大于 $A_x$。
这就变成了一个可行性背包问题。
要对于每个 $A_x$ 求出答案,故问题等价于 : 给出一棵 01Trie ,求每个叶节点祖先路径的分叉大小的可行性背包。
对 01Trie 进行 `dfs` ,向某个分支走时,当前背包内加入另一个分支。
可行性背包可以利用 `bitset` 优化,又注意到有多个儿子的点(需要添加物品)的个数是 $O(n)$ 的,故总复杂度为 $O(n^2/w)$。
询问离线回答。
------------
根据 **性质①** ,若将虚树中的无分叉链简化成一条边,则虚树的状态量就会被简化到 $O(|S|)$ 级别,这正是我们想要的效果。
(当然,无分叉链简化成一条边的同时,链条中的信息也要进行简化,具体方式因具体题目而异)
为了方便,下文中,我们将简化后的虚树简称为“虚树”,而将原定义中的虚树改称为“完整虚树”。
![](https://cdn.luogu.com.cn/upload/image_hosting/3vw9qlrw.png)
接下来,我们需要快速建立虚树。
首先找出虚树中的所有点。不难发现,多度点一定是两个关键点的 $\rm lca$ ,我们只需求集合 $\{{\rm lca}(u,v)|u,v\in S,u\neq v\}$ 即可得到所有多度点。
- $\color{blue}\Large\star$ **性质②** $\{{\rm lca}(u,v)|u,v\in S,u\neq v\}=\{{\rm lca}(u_i,u_{i+1})\}$ 其中 $u_i$ 按照 `dfs` 序从小到大排序。
**证明** : 首先显然有左边 $\supseteq$ 右边,那么只需证左边 $\subseteq$ 右边。
对于多度点 $t$ ,找出其(在虚树中)的所有直接儿子 $p_1,p_2,...p_m$ ,按 `dfs` 序排序。
任取其中相邻的两个儿子 $u,v$。找出 $u$ 子树内 `dfs` 序最大的关键点 $u'$,以及 $v$ 子树内 `dfs` 序最小的关键点 $v'$ ,则 $u',v'$ 是 `dfs` 序相邻的两个关键点,且有 ${\rm lca}(u',v')=t$。
![](https://cdn.luogu.com.cn/upload/image_hosting/66uxizy7.png)
因此,我们将关键点集合按照 `dfs` 序排序后,对相邻点求 $\rm lca$ (并去重)即可得到虚树中的点集。
- $\color{red}\Large\star$ **应用例**
几个相似的结论。
- 虚树边权和 / 链并
点集 $S=\{u_0,u_1,u_2,...u_{m-1}\}$ 的完整虚树的边权和可以这样计算 :
$$\frac{1}{2}\sum\limits_{i=0}^{m-1}dis(u_i,u_{i+1\bmod m})$$
**例题** : [P3320 [SDOI2015]寻宝游戏](https://www.luogu.com.cn/problem/P3320)
- 虚树式树上差分
若要对点集 $S$ 的完整虚树上的每个点加 $c$ ,可以进行如下操作 :
在每个 $u\in S$ 处 $+c$ ,在每个 $dis(u_i,u_{i+1\bmod m})$ 处 $-c$ ,最终做子树和即可得到答案。
**例题** : [[DS记录]Bzoj#4771. 七彩树](https://www.luogu.com.cn/blog/command-block/bzoj4771-qi-cai-shu)
------------
接下来考虑如何连接虚树中的边。
将虚树点集 $S'=\{u_1,u_2,...u_m\}$ 按照 `dfs` 序排序后,有如下算法可以求出边集。
模拟对虚树进行 `dfs` 的过程,用栈维护根到当前点的路径。按照 `dfs` 序依次考虑各个点,当新加入点 $u$ 时,一直弹栈(回溯)直到 $u$ 在栈顶的子树内。将 $u$ 与栈顶连边,并入栈。
具体实现可见例题。
## 经典实现
- **例题** : [P4103 [HEOI2014]大工程](https://www.luogu.com.cn/problem/P4103)
**题意** : 给出树 $T$ ,边有边权,均为正。
多组询问,每次给出关键点集合 $S$ ,求下列式子的值 :
$$
\begin{aligned}
\sum\limits_{u\in S,v\in S,u\neq v}dis(u,v)\\
\max\limits_{u\in S,v\in S,u\neq v}dis(u,v)\\
\min\limits_{u\in S,v\in S,u\neq v}dis(u,v)
\end{aligned}
$$
建立点集 $S$ 的虚树,将一条无分叉链简化为一条长度为链中所有边长度总和的边。
对于问题一,分别考虑每条边的贡献,系数为两侧关键点数目的乘积。
对于问题二,记 $f[u]$ 表示 $u$ 到子树内最远关键点的距离。对于每个点找出两个不同子树内的最远点进行转移。
对于问题三,记 $g[u]$ 表示 $u$ 到子树内最近关键点的距离。对于每个点找出两个不同子树内的最近点进行转移。
复杂度为 $O(n\log n)$。
```cpp
```
- **更多例题**
- [CF613D Kingdom and its Cities](https://www.luogu.com.cn/problem/CF613D)
- [P2495 [SDOI2011]消耗战](https://www.luogu.com.cn/problem/P2495)
- [P3233 [HNOI2014]世界树](https://www.luogu.com.cn/problem/P3233)
- [P4242 树上的毒瘤](https://www.luogu.com.cn/problem/P4242)
- [CF809E Surprise me!](https://www.luogu.com.cn/problem/CF809E)
- [P4606 [SDOI2018]战略游戏](https://www.luogu.com.cn/problem/P4606)
- [P7422 「PMOI-2」城市](https://www.luogu.com.cn/problem/P7422)
## 更精细的实现
考虑利用已按照 `dfs` 序排序的点集 $S$ 直接得到虚树。
仍然维护栈记录根到当前点的路径。
加入点 $u$ 时,分类讨论。
- 情况一 : $u$ 在栈顶 $top$ 的子树中。
直接将 $u$ 加入栈中。
- 否则
求 $t={\rm lca}(top,u)$ 。一直弹栈,直到 $t$ 在栈的次顶的子树内。
如图所示 :
![](https://cdn.luogu.com.cn/upload/image_hosting/f1r3v2z6.png)
红色为新连接的边。
- 情况二 : $t=top$
直接将 $u$ 加入栈中。
- 情况三 : $t\neq top$
如上图,此时将 $top$ 弹出,然后加入 $t,u$。连边 $t\rightarrow u$。
该算法的核心在于,按照 `dfs` 序加入时,新点所能接入的位置的总是一条右链,而随机插入时则可能在整颗树的任何位置。
若对随机插入时的情况感兴趣,可见 [题解 P6071 【[MdOI2020] Treequery】](https://www.luogu.com.cn/blog/command-block/solution-p6071)
该做法常数较小,且可以利用 $O(1)\ \rm LCA$ 在点集已经排序的情况下线性建立虚树。
## 其他性质 & 套路
- $\color{red}\Large\star$ **分治与虚树**
在某些题目中,需要对点集进行分治的同时建立虚树。
此时,可以利用归并维护点集的 `dfs` 序,再利用上述算法线性建立虚树。相较于朴素的实现可以省去一个 $\log$。
另外一种方法是,自上而下建立,先建立原问题的虚树 $T$ ,再在 $T$ 上 `dfs` 建立两个子问题的虚树。(子问题中的虚树是原问题中的虚树的虚树)
- **例题**
- [[DS记录]P5439 【XR-2】永恒](https://www.luogu.com.cn/blog/command-block/ds-ji-lu-p5439-xr-2-yong-heng)
- $\color{red}\Large\star$ **正权(完整)虚树的封闭性**
在**正边权**的树中,一个经典结论是 : **点集直径的封闭性**
即 : 对于点集 $S_1,S_2$ ,若直径分别为 $(u_1,v_1),(u_2,v_2)$ ,则点集 $S_1∪S_2$ 的直径端点必然在 $u_1,v_1,u_2,v_2$ 之中。
借由该结论,我们可以快速计算点集并的直径。
正权(完整)虚树的封闭性是一个更强的结论 :
记 ${\rm val}(S)$ 为点集 $S$ 的完整虚树中的边权和。
定义某个点集 $S$ 的最大 $k$ 点虚树为 : $|S|=k$ 且 ${\rm val}(S)$ 最大的 $S$。(当 $k=2$ 时即退化为直径)
对于点集合 $S_1,S_2$ ,若最大 $k$ 点虚树分别为 $T_1,T_2$ ,则点集 $S_1∪S_2$ 的最大 $k$ 点虚树必然在 $T_1∪T_2$ 之中。
下面介绍如何快速计算点集并的最大 $k$ 点虚树。
将点集 $T_1∪T_2$ 建立虚树,然后以点集直径的一端为根,将其长链剖分,选取前 $k-1$ 条长链即可。
正确性可见 : [[DS记录]CF526G Spiders Evil Plan](https://www.luogu.com.cn/blog/command-block/ds-ji-lu-cf526g-spiders-evil-plan)
- $\color{red}\Large\star$ **特殊图上的虚树**
[[DS记录]P5360 [SDOI2019]世界地图](https://www.luogu.com.cn/blog/command-block/ds-ji-lu-p5360-sdoi2019-shi-jie-di-tu)