虚树

虚树

所谓虚树,就是对于一棵指定的树 \(T\),构造一棵新的树 \(T'\) 使得总节点数最小包含指定的某几个节点和它们的 LCA。OI Wiki 上的几张图就能说明:

vtree-3

vtree-4

vtree-5

vtree-6

虚树能解决什么问题?优化树形 DP。比如给定多组询问,每组询问给出树上的一些关键点,最终的答案只和这些关键点有关。那么此时我们如果每次都 \(O(n)\) 遍历一遍树的话总复杂度就会达到 \(O(nq)\sim O(nq\log n)\),超时;但建立虚树就会大大减少这个复杂度,最后的复杂度均摊在了 \(O(\sum k\log n)\) 级别。

建树

有两种建树方法,OI Wiki 上有讲。一般来说我们使用单调栈建树。

先预处理整棵树的 DFS 序和 LCA,然后对于每组询问的关键点构造虚树。

  1. 先对所有关键点 \(a_i\) 按照 DFS 序排序。
  2. 建一个栈 \(\text{stk}\),满足 \(\text{stk}(1)=\mathit{rt}\)\(\text{stk}(\mathit{top})=a_k\)\(\text{stk}(x)\)\(x-1\) 的后代。每当 \(v\) 被弹出栈时,建立 \(u\to v\) 的边。当我们要给栈里加入一个新的节点 \(x\) 时,设 \(l=\operatorname{LCA}(x,\text{stk}(\mathit{top}))\),分类讨论:
    • \(l=\text{stk}(\mathit{top})\),也就是 \(x\)\(\text{stk}(\mathit{top})\) 子树内的节点,此时直接将 \(x\) 入栈;
    • \(l\ne\text{stk}(\mathit{top})\),也就是 \(x\) 不是 \(\text{stk}(\mathit{top})\) 子树内的节点,此时我们把栈弹出,每弹出一个节点就加一条边,直到变成第一种情况。
  3. 最后,把尚未弹出栈的节点依次弹出,并连边。

最后的实现是这样的:

int st[MAXN],tp;
void ins(int x){
	if(!tp) return st[++tp]=x,void();
	int l=lca(st[tp],x);
	while(tp>1&&dep[l]<dep[st[tp-1]]){
		addedge(st[tp-1],st[tp]);
		--tp;
	}
	if(dep[l]<dep[st[tp]]) addedge(l,st[tp]),--tp;
	if(!tp||st[tp]!=l) st[++tp]=l;
	st[++tp]=x;
}

int main(){
    ...
    for(int i=1,k;i<=m;++i){
        k=read();
        for(int j=1;j<=k;++j) a[j]=read();
        sort(a+1,a+k+1,[](int x,int y){return dfn[x]<dfn[y];});
		if(a[j]!=1) st[++tp]=1;
		for(int j=1;j<=k;++j) ins(a[j]);
		if(tp) while(--tp) addedge(st[tp],st[tp+1]);
	}
}

其实,这个建树的板子还是直接背最方便。

清空

虚树的清空是很需要注意的,为了保持复杂度正确,我们不能 memset 之类,只能每次扫关键点地清空,或者在 DFS 函数的最后清空。下面的例题有这一点。

P2495 [SDOI2011] 消耗战

题意:给定一棵 \(n\) 个点的树和 \(m\) 个询问,边有边权。每个询问给定一些树上的关键点,求出使 \(1\) 节点不能到达任何关键点所需要断开的最少边权和。 \(2\le n\le2.5\times10^5\)\(\sum k_i\le5\times10^5\)

看到这种 \(\sum k_i\le5\times10^5\) 的就差不多是虚树了。

首先容易得到的是一个 \(O(nq)\) 的 DP:设 \(f_i\) 为使 \(i\) 不与其子树中的任意一个关键点连通的最小代价。显然,枚举 \(i\) 的儿子 \(v\)

  • \(v\) 是关键点,\(f_i\gets f_i+w(i,v)\)
  • \(v\) 不是关键点,\(f_i\gets f_i+\min\{f_v,w(i,v)\}\)

但是我们发现因为 \(k\) 很稀疏,每一次把所有点都遍历一遍是不必要的,所以我们可以建虚树,只保留关键点,然后在虚树上进行 DP,这样的复杂度就是对的。

所以把虚树建出来,然后在虚树上进行 DP 就可以了。DP 实际上不是本题的难点。

// 前面的快读、树剖不再展示
int st[MAXN],tp;
bool vis[MAXN];
void ins(int x){
	if(!tp) return st[++tp]=x,void();
	int l=lca(st[tp],x);
	while(tp>1&&dep[l]<dep[st[tp-1]]){
		addedge(st[tp-1],st[tp]);
		--tp;
	}
	if(dep[l]<dep[st[tp]]) addedge(l,st[tp]),--tp;
	if(!tp||st[tp]!=l) st[++tp]=l;
	st[++tp]=x;
}
ll dfs3(int u){
	ll sum=0;
	for(int i=head[u];i;i=e[i].to) sum+=dfs3(e[i].v);
	ll res=vis[u]?dd[u]:min(sum,dd[u]);
	vis[u]=0;
	head[u]=0;
	return res;
}

int main(){
	n=read();
	for(int i=1,u,v,w;i<n;++i){
		u=read(),v=read(),w=read();
		addedge(u,v,w),addedge(v,u,w);
	}
	dd[1]=2e18;
	dfs(1,0);
	dfs2(1,1);
	memset(head,0,sizeof(int)*(n+5));
	tot=0;
	m=read();
	for(int i=1,k;i<=m;++i){
		k=read();
		for(int j=1;j<=k;++j) a[j]=read(),vis[a[j]]=1;
		sort(a+1,a+k+1,[](int x,int y){return dfn[x]<dfn[y];});
		st[++tp]=1;
		for(int j=1;j<=k;++j) ins(a[j]);
		if(tp) while(--tp) addedge(st[tp],st[tp+1]);
		write(dfs3(1));
		tp=tot=0;
	}
	return fw,0;
}

注意到原树在求出 DFS 序和 LCA 之后就没有用了,所以虚树可以直接使用原树的链式前向星建树。注意我们在 DFS 函数的末尾就把 vishead 清空了,这样才能保证复杂度的正确。


虚树的题本来想再贴几道,结果发现要么可以不拿虚树做,要么难点不在虚树。其实大多数虚树的题难点还是在 DP 上,虚树就是个板子,建完了之后调用就可以。

所以就不贴了。

posted @ 2025-01-27 21:29  Laoshan_PLUS  阅读(281)  评论(2)    收藏  举报