问题引入
在一类树上动态规划问题中,题目给出的询问往往包含树上的很多各节点,并保证总的点数规模小于某个值.
如果我们直接在整颗树上进行dp的话,时间复杂度与询问的次数有关,这显然是不可接受的,如果我们可以找到一种动态规划的方法,使其时间复杂度与询问中点的实际规模相关就好了.
虚树
虚树即是一颗虚拟构建的一棵树,这个树只包含关键点(一般就是题目给出的点)以及关键lca的点,而其他不影响虚树结构的点和边都相当于进行了路径压缩,整颗虚树的规模不会超过关键点数目的两倍.
包含关键点 1 3 7 8 的虚树
其中6是关键lca节点
很显然,其他不是那么关键的点及边形成的路径我们都将他们压缩到了一条边,例如在第二个虚树中,我们相当于把1−6的路劲压缩到了边1−6中,而9号节点这种非关键点我们直接扔掉了,因为我们在dp的时候不会用到9号点.
虚树构建
预处理我们对整颗树得到dfs序列(即前序遍历),记为dfn[u].
我们使用一个栈,从栈顶到栈底的元素形成虚树的一颗树链.
当我们得到一些**询问点(关键点)**的时候,对这些点按照他们的dfn[u]值进行排序,然后从dfn值小的开始扫描,结合栈中保存的树链信息就可以将这颗虚树构建出来.
假设我们当前扫到的关键点为u,栈指针为top,栈为stk.
根节点一定是在链上,所以1一定要入栈
- 如果栈为空,或者栈中只有一个元素,那么显然应该:
stk[++top]=u;
- 取lca=LCA(u,stk[top]),如果lca=stk[top],则说明u点应该接着stk[top]点延长当前的树链.做操作:
stk[++top]=u;
- 如果lca≠stk[top],则说明u与stk[top]分属lca的两颗不同的子树,且包含stk[top]的这颗子树应该已经构建完成了,我们需要做的是: 将lca的包含stk[top]子树的那部分退栈,并将这部分建边形成虚树.如果lca不在栈(树链)中,那么要把lca也加入栈中,保证虚树的结构不出现问题,随后将u加入栈中,以表延长树链.
看了这么多不如看代码:
inline void vbuild(int x) {
if (top == 1) { stk[++top] = x; return; }
int lca = LCA(x, stk[top]);
if (lca == stk[top]) return;
while (top > 1 && dfn[lca] <= dfn[stk[top - 1]])
ve[stk[top - 1]].pb(stk[top]), --top;
if (lca != stk[top]) ve[lca].pb(stk[top]), stk[top] = lca;
stk[++top] = x;
}