虚树
好文(maybe):
虚树,听起来深奥,但其本质是不难的。
一棵树,如果想对其进行 \(q\) 次询问,每次询问都要做一遍树形 dp,那么时间复杂度是 \(O(qn)\)。但如果每次询问的关键点总和很小。那么虚树就适合这类题目。
虚树将关键点与必要的 \(\text{lca}\) 进行连边,使得选出来的点还是一棵树,但节点规模小。换言之,所有关键点肯定都要出现在虚树上,我们可以选出一些两两关键点的 \(\text{lca}\) 放入虚树中,保证 dp 可以按照原来的树形 dp 的顺序进行。
单调栈建虚树
- 此处的单调栈不是维护序列的最值,而是维护一条从根节点开始的右链,从而
dfs序是单调递增的。
将所有关键点按 dfs 序排序,依次考虑每个关键点,记 \(L = \text{lca} (stack_{top},x)\),其中 \(x\) 是一个关键点,依据 \(x\) 是否跟 \(stack_{top}\) 在同一条链上进行分类讨论。
-
\(L=stack_{top}\)。则 \(stack_{top}\) 是 \(x\) 的祖先且两个点位于同一个右链上,直接让 \(x\) 入栈就好了。
-
\(L \neq stack_{top}\) 。说明 \(x\) 不与 \(L\) 在同一条链上。因为 \(L = \text{lca} (stack_{top},x)\),所以 \(L\) 肯定是当前右链上的点,但可能在栈上或者不在栈中。
为了使 \(L\) 与 \(x\) 连接,我们希望一直退栈,使得 \(dfn_{stack_{top-1}} \le dfn_L < dfn_{stack_{top}}\),同时一边退栈一边建边 \(stack_{top-1} \rightarrow stack_{top}\)。
再对 \(L\) 是否等于 \(stack_{top-1}\) 进行分类讨论,原因是判断 \(L\) 是否已经入栈:- \(L = stack_{top-1}\),说明 \(L\) 已经入栈,那么连边 \(L \rightarrow stack_{top}\),将 \(stack_{top}\) 退栈。
- \(L \neq stack_{top-1}\),说明 \(L\) 还没入栈,连边 \(L \rightarrow stack_{top}\),将 \(stack_{top}\) 退栈,将 \(L\) 入栈。
最后栈中还有一条右链还没建边,对于 \(1 \le i < top\),连边 \(stack_i \rightarrow stack_{i+1}\)。
虚树就建完啦!(来自 oi-wiki)
code
rep(i,1,k){
if(a[i]==1)continue;
int l=lca(st[top],a[i]);
if(l!=st[top]){
while(top>=2 && dfn[l]<dfn[st[top-1]]){
add_virtual_tree(st[top-1],st[top]);
--top;
}
if(l!=st[top-1]){
vhead[l]=0;
add_virtual_tree(l,st[top]);
st[top]=l;
}else{
add_virtual_tree(l,st[top]);
--top;
}
}
vhead[a[i]]=0;
st[++top]=a[i];
}
正确性证明
虚树的清空
上一次的关键点并不适用下一次,所以一定要清空。清空时要注意,不要让时间复杂度退化为 \(O(qn)\)。
oi-wiki 上给出了一种清空虚树的巧妙形式。此处做一个引用:
所以我们在 有一个从未入栈的元素入栈的时候清空该元素对应的邻接表 即可。
其实就是将对当前询问有用的点的 head 数组清空。
模板题
P2495 【模板】虚树 / [SDOI2011] 消耗战。
看到标题关键点果断建立虚树。
接下来考虑树形 dp。
记 \(f_i\) 表示点 \(i\) 与其子树中所有关键点不连通的最小代价。
设 \(j \in son(i)\),分情况写转移方程。
- 若 \(j\) 为关键点,那么 \(f_i \leftarrow f_i + w(i,j)\)。
- 若 \(j\) 不是关键点,那么 \(f_i \leftarrow f_i + min(w(i,j),f_j)\)。
建出虚树中,转移的边相当于被压缩了,对于被压缩的一条链,其有用的 dp 信息是边权的最小值,于是这一部分可以倍增或者树剖+线段树维护。这道题就做完了。
例题1
\(\sum k\) 在 \(O(n)\) 数量级,考虑建出虚树。
-
\(\max\) 和 \(\min\) 是好做的,枚举虚树上的点作为路径的 \(\text{lca}\),这样答案可以由 \(x \rightarrow \text{lca}(x,y)\) 和 \(\text{lca}(x,y) \rightarrow y\) 拼接,一边 dp 一边统计答案。
注意:由于树是单位权,\(\min\) 理论上来说只要取一条从 \(x\) 到 \(subtree(x)\) 中的 \(\min\) 值即可(\(x\) 在虚树上)。但我们统计的是两两关键点之间的 \(\min\),除非令一个关键点为树根,但关键点是会随询问变化的,所以不行。那我们只能用两条路径拼起来,特殊地,当 \(x\) 是关键点时,\(mn_x = 0\),和上述情况是等价的,却具有更强的性质。
-
接下来要解决路径总和的问题。记 \(cnt_x\) 表示 \(x\) 子树内关键点的数量,\(sum_x\) 表示 \(x\) 子树内关键点到 \(x\) 的路径总和。
这种两两路径的 dp 问题常见的转移顺序是 \(x\) 一边合并儿子 \(y\) 的信息一边往下 dp。如下图所示:

巧妙转化:拆贡献。如下图所示:

将总贡献拆成绿蓝两条路径。绿的路径贡献是 \((sum_x + cnt_x \times dis(x,y)) \times cnt_y\),蓝的路径贡献是 \(sum_y \times cnt_x\)。其中 \(dis(x,y) = dep_y -dep_x\)。于是树形 dp 做一下就好了。

浙公网安备 33010602011771号