0%

虚树

虚树

什么是虚树

虚树常常被使用在树形dp中,就比如这题。当一次询问仅仅涉及到整颗树中少量结点时,为每次询问都对整棵树进行dp在时间上是不可接受的。此时,我们建立一颗仅仅包含部分关键结点的虚树,将非关键点构成的链简化成边或是剪去,在虚树上进行dp。

虚树包含所有的询问点及它们之间的LCA。显然虚树的叶子节点必然是询问点,因此对于某次含有k个点的询问,虚树最多有K个叶子结点,从而整颗虚树最多只有2k-1个结点(这会在虚树变成二叉树形态时达到)。

建立虚树之前

我们需要:

预处理出原树的dfs序以及dp可能用到的一些其他东西。

高效的在线LCA算法,单次询问O(logn)的倍增和树剖,O(1)的RMQ−ST皆可。

将询问点按dfs序排序。

如何建立虚树

最右链是虚树构建的一条分界线,表明其左侧部分的虚树已经完成构建。我们使用栈stak来维护所谓的最右链,top为栈顶位置。值得注意的是,最右链上的边并没有被加入虚树,这是因为在接下来的过程中随时会有某个lca插到最右链中。

初始无条件将第一个询问点加入栈stak中。

将接下来的所有询问点顺次加入,假设该询问点为now,lc为该点和栈顶点的最近公共祖先即lc=lca(stak[top],now)。

由于lc是stak[top]的祖先,lc必然在我们维护的最右链上。

考虑lc和stak[top]及栈中第二个元素stak[top-1]的关系。

情况一

lc = stak[top],也就是说,now在stak[top]的子树中

这时候,我们只需把now入栈,即把它加到最右链的末端即可。

情况二

lc在stak[top]和stak[top-1]之间。

显然,此时最右链的末端从stak[top-1]->stak[top]变成了stak[top-1]->lc->stak[top],我们需要做的,首先是把边lc-stak[top]加入虚树,然后,把stak[top]出栈,把lc和now入栈。

情况三

lc=stak[top−1]。

这种情况和第二种情况大同小异,唯一的区别就是lc不用入栈了。

情况四

此时有dep[lc]<dep[stak[top-1]]。lc已经不在stak[top-1]的子树中了,甚至也未必在stak[top-2],stak[top-3]……的子树中。

以图中为例,最右链从stak[top-3]->stak[top-2]->stak[top-1]->stak[top]变成了stak[top-3]->lc->now。我们需要循环依次将最右链的末端剪下,将被剪下的边加入虚树,直到不再是情况四。

就上图而言,循环会持续两轮,将stak[top],stak[top-1]依次出栈,并且把边stak[top-1]-stak[top],stak[top-2]-stak[top-1]加入虚树中。随后通过情况二完成构建。

当最后一个询问点加入之后,再将最右链加入虚树,即可完成构建。

另一种虚树构建方法:欧拉序

什么是欧拉序

正常的dfs序仅在入栈的时候计算一次,而欧拉序,不仅在入栈的时候计算一次,还在出栈的时候计算一次,也就是说一个点有两次出现机会压栈为+,弹栈为-,欧拉序记录了dfs的全部信息,只要有了这个树的欧拉序,就算没有树,给我们一个栈,照样可以在树上跑dfs。

我们发现,抽出来的那只树,它的欧拉序大小关系和原来的树的欧拉序是一样的,所以只需要把所有点的复制一个弹出点,然后把压栈点和弹栈点按原来树上的欧拉序排一波序,就是新树中的点按欧拉序排序的结果,然后欧拉序我们有了,直接不建树跑dfs即可

------ THEEND ------

欢迎关注我的其它发布渠道