虚树
说在前面
有一类问题,整棵树的节点数非常多,但是询问只和其中的若干个关键节点(以及必要的维护他们之间的连通性的节点)有关,这类问题我们可以采用虚树来优化。
构建
一般采用单调栈构建虚树,首先将所有关键节点按 \(dfn\) 排序,把 \(1\) 号节点入栈。
单调栈中维护的一直是一条链,我们扫一遍所有的节点,判断这个节点和栈顶的 LCA 是不是栈顶,如果不是就进入判断阶段,否则说明二者在一条链上。
判断次大节点的 dfn 是否比 LCA 大,如果是,说明次大节点的深度大(栈顶更大),弹出栈顶并与次大节点连边。
判断一下 LCA 是否等于当前的次大节点,如果不是则弹出栈顶并连与 LCA 连边,把 LCA 入栈。
如果不是说明 LCA 已经是次大节点了,直接弹出栈顶并与 LCA 连边即可。
最后把当前节点入栈,继续遍历。
code:
点击查看代码
void build(int n){
sort(imp + 1, imp + n + 1, cmp);
memset(head, 0, sizeof(head));
cnt = 0;
s[top = 1] = 1;
for(int i = 1; i <= n; i++){
if(imp[i] == 1) continue;
int lc = lca(s[top], imp[i]);
if(lc != s[top]){
while(dfn[s[top-1]] > dfn[lc])
add(s[top], s[top-1]), top--;
if(lc != s[top-1])
add(s[top], lc), s[top] = lc;
else add(lc, s[top]), top--;
}
s[++top] = imp[i];
}
for(int i = 1; i < top; i++)
add(s[i], s[i+1]);
}
例题
朴素的树形 DP 很好想,要么是切断当前节点,要么是切断所有儿子,维护一下到根节点的路径上的最小值,转移即可。
但是每次都做一遍复杂度是 \(O(nm)\),非常不乐观,所以考虑构建虚树后在虚树上DP即可。
注意到虚树的构建过程天然适合我们清空树,每次把节点入栈前清空即可(如果memset/fill会收获TLE的好成绩)。
同样的建立虚树,然后考虑 DP,问题 1 直接转成每条虚树上的边(对应原树上的一条路径,长度即为虚树上两点的深度差)被多少路径经过,dfs 统计之,问题 2、3 类似树的直径,统计到子树中的关键点最小/最大长度,直接求即可。
练习题
P3233 [HNOI2014] 世界树
#6184. 无心行挽
P4220 [WC2018] 通道
不言而喻