虚树

虚树

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

vtree-3

vtree-4

vtree-5

vtree-6

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

建树

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

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

  1. 先对所有关键点 ai 按照 DFS 序排序。
  2. 建一个栈 stk,满足 stk(1)=rtstk(top)=akstk(x)x1 的后代。每当 v 被弹出栈时,建立 uv 的边。当我们要给栈里加入一个新的节点 x 时,设 l=LCA(x,stk(top)),分类讨论:
    • l=stk(top),也就是 xstk(top) 子树内的节点,此时直接将 x 入栈;
    • lstk(top),也就是 x 不是 stk(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 节点不能到达任何关键点所需要断开的最少边权和。 2n2.5×105ki5×105

看到这种 ki5×105 的就差不多是虚树了。

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

  • v 是关键点,fifi+w(i,v)
  • v 不是关键点,fifi+min{fv,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 @   Laoshan_PLUS  阅读(45)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示