这次要学的是一个听起来很虚的东西
没错写起来更虚
毕竟都是在虚的东西上面操作……
虚树,顾名思义,就是一棵不真实的树【大雾】
它可以对于一部分点保存整棵树的所有信息,而对一部分点选择忽略,这样可以增加dp/点分治的效率
为了给大家一个更好的例子(免得看不懂虚树到底能干嘛),我们先来看一个题目:消耗战
容易看出,这道题需要对每个询问做树形dp,然后每次在原树上dp都有$O\left(n\right)$的时间开销,m次询问做完肯定TLE
那么怎么解决呢?可能有些人会说离线瞎搞,但是万一这道题强制在线呢???
这时候就要用到虚树这个数据结构了
考虑一组询问,我们在树上有一些点是”询问点“,剩下的点不是
也就是说,其实我们只是需要对这些”询问点“处理信息,然后再把询问点的信息合并就好了
诶,等等,万一询问点之间不全都是父子关系怎么办?
那看来我们还要加上询问点们的LCA,也一起放到这课树里面
这样,我们的新树中就有这组询问的所有询问点,以及它们互相之间的LCA了,然后这个点的数量级是$O\left(询问中的点数\right)$级别的
我们看到,题目数据范围里说,所有的询问点数总和不超过300000
OK!这样dp就不会超时了!!
所以上面这一步中,我们找到了做题的思路:把虚树建出来,在虚树上dp
那么,怎么实现构建虚树的过程呢?
这个好像有点麻烦,因为我们并没有什么方法能对于一个点集合求出它们的附加LCA,所以我们得换个角度下手
这里的思路比较复杂,说出来也价值不大,所以就直接提供方法了
我们维护一个单调的栈,这个栈里的元素关于深度单调,也就是说我们栈里面保存一条从当前根开始的树链。
先把所有的”询问点“按照dfs序排个序,然后依次把它们加入栈中
每一次,我们设grand为当前待入栈节点和当前栈顶节点的lca
然后做这样一个循环:
如果当前的栈顶深度大于grand,我们就在栈顶和栈的第二个元素中间连一条虚树边,并把栈顶弹掉,直到栈顶的深度小于等于grand
此时让grand和上一个被弹出的元素连边
这时再把grand加入栈顶,然后再把待入栈节点加进来
所有的”询问点“都入栈了以后,再把栈里的元素一个一个弹掉并连边就好了
注意这个过程中,所有的加边只在弹栈过程中进行,注意不要写错
这样建出来的虚树就是比较点数少的了,而且在求dfs序的同时我们还可以维护深度和子树大小之类的信息,后面在虚树上依旧可以使用
不过,有的时候(例如上面的例题),某个节点一定不会作为询问点,此时可以先把这个节点(比如例题中的一号)加入栈,作为虚树的根
否则就需要在每一次新加入询问点的时候判断栈是否为空,如果是空的就要直接把这个节点入栈
这样的方法,最后栈里剩下的那个元素,就是虚树的根了
当然,有的题目因为需要一些和dfs时确定的根节点(比如1号)有关的信息,所以必须以一号作为根,这种时候就要在dp里面判断一下了
说了这么多,其实虚树的建法也大都和题目有关,因此还是要多看题
这里放三道虚树例题,分别对应上面那段讲的三种情况
SDOI2011消耗战
HEOI2014大工程
HNOI2014世界树
可以看到虚树一般作为优化dp时间复杂度的一种手段,题目真正的精髓还在dp上
像世界树这道题就dp非常恶心,写起来贼难受......
总结一下,虚树就是一个优化树的结构的数据结构【好绕啊】,一般结合dp或者点分治使用(好像比较少见点分治的)
当然可能有什么结合树上莫队啊启发式算法啊之类的恶心题,但是见得不多就是了
实际上,虚树这个算法源于去除冗余信息的思维,它把多的、不需要的信息集成在了虚树边上
这一点在世界树那题里面特别体现了出来