虚树
本文最后更新于 2026年2月21日 下午
-1 鲜花
久仰大名,先前在相当多的模拟赛或者正式比赛的题目中都见到了,但是一直没有认真学过,现在又来严肃填坑。
0 前置芝士
- 单调栈
- 复杂度优秀的在线LCA(如倍增/树剖/四毛子)
- 树形dp
1 前言
虚树这个东西非常好理解而且好用,其核心思想在于 ”随用随取“。因为往往在一个树形结构中,并非所有的点都是我们需要处理的,甚至有大量冗余信息,如果能建出一颗树只保留原本这棵树的大致结构和我们需要的信息,往往可以显著降低复杂度,从而让看起来不对的暴力拿很多分甚至通过。
2 原理
一道例题:【模板】虚树 / [SDOI2011] 消耗战。
不考虑数据范围,DP显然是好想的。令 表示 子树中所有关键点全部被断开的最小代价,显然可以枚举 的每一个儿子 来转移:
- 若 为关键点,则一定要切断当前边,即
- 若 不是关键点,则显然可以选择切断当前边或者选择切断 子树内一些边的方案,即
但是发现直接这么做肯定是会爆掉的,但是关键点的数量很少。我们发现在整个转移过程中有用的点事实上只有三种:
- 关键点
- 根节点
- 任意两个关键点的 LCA
所以考虑建一颗虚树只保留这些东西。
建立虚树的方式有两种,下面介绍一种我个人很喜欢的单调栈建法。
单调栈建虚树的本质是将目前的虚树分为了两部分:已经建好的和“最右链”,并且用单调栈维护这一条“最右链”,因为一条链的 序一定是自下而上递减的。所谓“最右链”就是你这一条链上还能够继续添加东西的一条链,我们钦定在其左边的部分都是已经完成的,在其右边的部分都是还没建的。
先算出整棵树的 序,将关键点按照 序从小到大排序后进行如下操作。
-
初始化,清空虚树和关键点标记,将根节点入栈。
-
加入一个点,分类讨论属于以下哪种情况:
-
属于当前最右链上的一点。这种情况直接入栈即可
-
不属于当前最右链上。这种情况我们应该更新最右链,也就是把已经不属于最右链的点出栈并加到虚树中。我们令 和 分别表示栈顶和栈的次顶端, 表示新加入的点和 的LCA。
- 若 为 和 之间的一个点,则显然需要把边 加入虚树。然后弹出栈顶并且加入 。
- 若 为 ,则跟上面相同,只是不需要再重复加入 了。
- 若 为 更上面的点,则显然 下方的链都不属于最右链了,应该将其全部弹出并且都加入虚树。然后再分为上面的情况进行讨论。
-
需要注意的是,上述过程完成后都需要将要加入到点加入栈中。同时,所有虚树的边权都应为原图对应路径上的最小边权,因为切断关键点时可以选择代价最小的切断。
-
-
最后把剩余栈中的链也加入虚树,到这里虚树就建好了。
然后在新建的虚树上跑DP就可以了。DP的逻辑和原本的完全一致。
然后你就学会虚树了/bx/bx/bx。
3 实现
虚树建立完成后还需要注意的问题就是清空,可以考虑跑完DP再随便写一个清空的dfs就可以了。
丑陋的部分代码如下:
1 | |
4 时间复杂度分析
建立虚树的复杂度为 ,瓶颈在于对关键点排序和每加入一个点求 ,当然如果用四毛子可以规避掉后者。
每次建立的虚树至多有不超过 个顶点。因为有 个关键点,相邻两个点的LCA共 个点,以及如果没有给补上的根节点。
虚树大小确定了,树上DP的过程显然是 的。因此总复杂度 。
5 例题
我的题解:P4103 题解。