树上问题(长期更新)
虚树
在树上做一些东西的时候(废话),我们发现有很多树上的点是没有用的。
虚树就是对树上信息高度概括的技术,在虚树上,我们只保留最为有用的信息,且保持祖先后代关系不变。
具体而言,我们称一些点为关键点,即只有这些点会对问题的答案产生影响。
那么我们发现,关键点的LCA也是关键点。
当我们钦定树上的\(k\)个点作为关键点后,建出来的虚树最多有\(2k\)个点(感性理解)。
有一个事实:虚树里的点加多了也不会影响答案。这很显然。
那么为了方便,在建立虚树之前,我们把原树的树根\(1\)先加入虚树。
单调栈建立虚树
我们用栈维护虚树上的一条链,保证其中\(dfn\)单调递增。
起初,\(1\)在其中。
设现在的栈顶为\(tp\)。
我们尝试加入一个点\(u\)。
首先,找到\(l=LCA(u,tp)\)。
-
\(l=tp\)时,\(u\)就在这条链上,将\(u\)入栈即可。
-
否则,我们关注栈中次大结点(即栈顶下方的第一个元素)的\(dfn\)与\(dfn_l\)的大小关系。当\(dfn_l\)比其小时,将\(tp\)与栈中次大节点连边并退栈,重复此过程。跳出循环后,若\(l\)就是次大节点,那么把栈顶与\(l\)连边并退栈,否则将栈顶与\(l\)连边并退栈,然后将\(l\)入栈。最后将\(u\)入栈。
最后栈中还剩一条链,拿出来建边即可。
常数很小,若只考虑连边,是\(O(k)\)的,\(k\)为关键点的数量。
细节很多。使用链式前向星时,对于每个入栈的点,清空它的表头head
。
二次排序+LCA连边建立虚树
首先,将关键点按\(dfn\)排序,并将任意相邻两个关键点求出LCA,将关键点和LCA都扔到数组\(A\)中。
由于\(dfn\)的性质,此时\(A\)中已经包含了所有虚树中的点(感性理解)。那么现在就是要按原树中的祖先后代关系建边。
我们把\(A\)再次按\(dfn\)排序,此时第一个点一定是虚树的根(但不一定是原树的树根\(1\),这一点与单调栈做法不一样,要注意一下)。我们对任意两个\(A\)中相邻的点\(A_i\)和\(A_{i+1}\),连边\((\small LCA(A_i,A_{i+1}),A_{i+1}\big)\)。
第一个点是虚树的根,所以后面的LCA中肯定有一个是它,所以不管它了。
为什么这样是对的?
-
当\(LCA(A_i,A_{i+1})=A_i\)时,由于已经按\(dfn\)排序,\(A_i\)和\(A_{i+1}\)之间肯定没有其他关键点,所以直接连边就好。
-
当\(LCA(A_i,A_{i+1})\neq A_i\)时,同样由于已经按\(dfn\)排序,同上分析。
所以是对的。
常数较大。
使用时的细节
我们一般可以通过观察数据范围来判断是否在考察虚树(玄学)。
题目的数据范围里一般会有\(\sum\),最后的复杂度很可能是\(O(\sum k\log n)\)的。
虚树里的清空不能暴力去清,只能在进行计算/建树时顺带清空,这样才能保证复杂度。