- admin 的博客
虚树小记
- @ 2026-7-10 10:54:03
虚树小记 更新于 2026/7/5 16:15:19 作者
command_block
虚树是处理树上点集问题的有力而直观的工具。
提示 : 鉴于初次学习虚树的同学树论经验一般不丰富,建议初学者在一周目时不必同步关注形式化证明,优先感性理解虚树的形式以及核心算法。
标记代表关键性质, 标记代表扩展内容。
定义
- 给定一棵无根树 ,定义点集 的虚树(Virtual Tree)为 : 最小的,包含 的 的连通子图。
不难发现,虚树仍然是树。下面是两个虚树的例子,其中蓝色点为点集 ,带有红色边框的部分为虚树。

在处理与点集 有关的问题时,我们希望复杂度是和 有关,而不是和整棵树的大小 有关。
但根据虚树的定义,较小的 可能产生很大的虚树。(如 为一条链, 为链的两个端点,此时虚树为整个 )
所以,直接在未简化的虚树上统计并不能很好地优化复杂度。
考虑进一步挖掘虚树的性质,将其结构简化,使得信息量(需要处理的对象个数)降低到 级别。
经典性质
不妨选定节点 为根,将树 变为有根树,点集的虚树也随之变为有根树。
-
性质① 虚树中,儿子数 的点的数目 。
证明 : 在虚树上进行
dfs。称 中的点为“关键点”。记 为节点 子树中的关键点集合。显然,所有在虚树中的 的 均不为空。
称儿子数 的点为“分叉点”。
某个分叉点有至少两个子树,该点的 集合相当于将子树的 集合合并。(若自己是关键点,也会将自己并入)
总共合并的次数不多于 次,故分叉点的个数也 。
这等价地意味着,虚树中无分叉链的条数 。(考虑所有关键点及分叉点的父边)
如图, 的点集产生的 条无分叉链 :

-
应用例
动态开点 Trie 可以看做是维护了一棵完整的 Trie 的虚树。
于是,插入 次的动态开点 Trie 的分叉点个数和无分叉链个数均为 。
该结论的一些应用 :
-
证明后缀树的状态数是 的。
-
设计压缩 01Trie 。例 : EA : 题解 P6136 【【模板】普通平衡树(数据加强版)】
-
设计空间复杂度 的压缩线段树合并。
-
例题 : PKUSC2019 D1T3
题意 : 给定长为 的数组 以及 个询问 ,每次询问是否存在一个 使得 异或 在所有数异或 中排第 。
。
首先用 中的所有数建立 01Trie 。若 的某一位为 ,则相当于将该层的所有点的左右儿子交换。
考虑如何处理单组询问。将 祖先路径的分叉都找出来,可以通过调整 使得某个分叉内的所有数小于或大于 。
这就变成了一个可行性背包问题。
要对于每个 求出答案,故问题等价于 : 给出一棵 01Trie ,求每个叶节点祖先路径的分叉大小的可行性背包。
对 01Trie 进行
dfs,向某个分支走时,当前背包内加入另一个分支。可行性背包可以利用
bitset优化,又注意到有多个儿子的点(需要添加物品)的个数是 的,故总复杂度为 。询问离线回答。
-
根据 性质① ,若将虚树中的无分叉链简化成一条边,则虚树的状态量就会被简化到 级别,这正是我们想要的效果。
(当然,无分叉链简化成一条边的同时,链条中的信息也要进行简化,具体方式因具体题目而异)
为了方便,下文中,我们将简化后的虚树简称为“虚树”,而将原定义中的虚树改称为“完整虚树”。

接下来,我们需要快速建立虚树。
首先找出虚树中的所有点。不难发现,多度点一定是两个关键点的 ,我们只需求集合 即可得到所有多度点。
-
性质② $\{{\rm lca}(u,v)|u,v\in S,u\neq v\}=\{{\rm lca}(u_i,u_{i+1})\}$ 其中 按照
dfs序从小到大排序。证明 : 首先显然有左边 右边,那么只需证左边 右边。
对于多度点 ,找出其(在虚树中)的所有直接儿子 ,按
dfs序排序。任取其中相邻的两个儿子 。找出 子树内
dfs序最大的关键点 ,以及 子树内dfs序最小的关键点 ,则 是dfs序相邻的两个关键点,且有 。
因此,我们将关键点集合按照 dfs 序排序后,对相邻点求 (并去重)即可得到虚树中的点集。
-
应用例
几个相似的结论。
-
虚树边权和 / 链并
点集 的完整虚树的边权和可以这样计算 :
$$\frac{1}{2}\sum\limits_{i=0}^{m-1}dis(u_i,u_{i+1\bmod m})$$例题 : P3320 [SDOI2015]寻宝游戏
-
虚树式树上差分
若要对点集 的完整虚树上的每个点加 ,可以进行如下操作 :
在每个 处 ,在每个 处 ,最终做子树和即可得到答案。
例题 : [DS记录]Bzoj#4771. 七彩树
-
接下来考虑如何连接虚树中的边。
将虚树点集 按照 dfs 序排序后,有如下算法可以求出边集。
模拟对虚树进行 dfs 的过程,用栈维护根到当前点的路径。按照 dfs 序依次考虑各个点,当新加入点 时,一直弹栈(回溯)直到 在栈顶的子树内。将 与栈顶连边,并入栈。
具体实现可见例题。
经典实现
-
例题 : P4103 [HEOI2014]大工程
题意 : 给出树 ,边有边权,均为正。
多组询问,每次给出关键点集合 ,求下列式子的值 :
$$\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}$$
建立点集 的虚树,将一条无分叉链简化为一条长度为链中所有边长度总和的边。
对于问题一,分别考虑每条边的贡献,系数为两侧关键点数目的乘积。
对于问题二,记 表示 到子树内最远关键点的距离。对于每个点找出两个不同子树内的最远点进行转移。
对于问题三,记 表示 到子树内最近关键点的距离。对于每个点找出两个不同子树内的最近点进行转移。
复杂度为 。
-
更多例题
更精细的实现
考虑利用已按照 dfs 序排序的点集 直接得到虚树。
仍然维护栈记录根到当前点的路径。
加入点 时,分类讨论。
-
情况一 : 在栈顶 的子树中。
直接将 加入栈中。
-
否则
求 。一直弹栈,直到 在栈的次顶的子树内。
如图所示 :

红色为新连接的边。
-
情况二 :
直接将 加入栈中。
-
情况三 :
如上图,此时将 弹出,然后加入 。连边 。
-
该算法的核心在于,按照 dfs 序加入时,新点所能接入的位置的总是一条右链,而随机插入时则可能在整颗树的任何位置。
若对随机插入时的情况感兴趣,可见 题解 P6071 【[MdOI2020] Treequery】
该做法常数较小,且可以利用 在点集已经排序的情况下线性建立虚树。
其他性质 & 套路
- 分治与虚树
在某些题目中,需要对点集进行分治的同时建立虚树。
此时,可以利用归并维护点集的 dfs 序,再利用上述算法线性建立虚树。相较于朴素的实现可以省去一个 。
另外一种方法是,自上而下建立,先建立原问题的虚树 ,再在 上 dfs 建立两个子问题的虚树。(子问题中的虚树是原问题中的虚树的虚树)
-
例题
-
正权(完整)虚树的封闭性
在正边权的树中,一个经典结论是 : 点集直径的封闭性
即 : 对于点集 ,若直径分别为 ,则点集 的直径端点必然在 之中。
借由该结论,我们可以快速计算点集并的直径。
正权(完整)虚树的封闭性是一个更强的结论 :
记 为点集 的完整虚树中的边权和。
定义某个点集 的最大 点虚树为 : 且 最大的 。(当 时即退化为直径)
对于点集合 ,若最大 点虚树分别为 ,则点集 的最大 点虚树必然在 之中。
下面介绍如何快速计算点集并的最大 点虚树。
将点集 建立虚树,然后以点集直径的一端为根,将其长链剖分,选取前 条长链即可。
正确性可见 : [DS记录]CF526G Spiders Evil Plan
- 特殊图上的虚树