虚树

好文(maybe):

虚树-OI-wiki

虚树-洛谷日报

虚树,听起来深奥,但其本质是不难的。

一棵树,如果想对其进行 \(q\) 次询问,每次询问都要做一遍树形 dp,那么时间复杂度是 \(O(qn)\)。但如果每次询问的关键点总和很小。那么虚树就适合这类题目。

虚树将关键点与必要的 \(\text{lca}\) 进行连边,使得选出来的点还是一棵树,但节点规模小。换言之,所有关键点肯定都要出现在虚树上,我们可以选出一些两两关键点的 \(\text{lca}\) 放入虚树中,保证 dp 可以按照原来的树形 dp 的顺序进行。

单调栈建虚树

  • 此处的单调栈不是维护序列的最值,而是维护一条从根节点开始的右链,从而 dfs 序是单调递增的。

将所有关键点按 dfs 序排序,依次考虑每个关键点,记 \(L = \text{lca} (stack_{top},x)\),其中 \(x\) 是一个关键点,依据 \(x\) 是否跟 \(stack_{top}\) 在同一条链上进行分类讨论。

  1. \(L=stack_{top}\)。则 \(stack_{top}\)\(x\) 的祖先且两个点位于同一个右链上,直接让 \(x\) 入栈就好了。

  2. \(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

P4103 [HEOI2014] 大工程

\(\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 做一下就好了。

record

posted @ 2026-04-02 14:14  lbh666  阅读(4)  评论(0)    收藏  举报