虚树学习笔记

虚树简介

详见 OI-Wiki

考虑每一次 dp 需要的点其实只有关键点本身和两两关键点的 LCA,所以没必要对整棵树进行 dp。

暴力求时间复杂度为 O(ki2) 的,那么如何快速求出两两关键点的 LCA?将关键点按 dfs 序排序后对相邻两点求 LCA 即可。

证明
假设当前序列已经是按 dfs 序排完序的序列,对于一组在序列中相邻的关键点,都可以看做是两颗不同子树中的点,那么对他们求 LCA 便可以求出这两颗子树的 LCA,此时所有在这两颗子树内的点的共同 LCA 就求出来了。所以 dfs 序相邻点的 LCA 组成的点集一定是包含两两 LCA 的点集的。

设加入 LCA 后的序列为 t,将 t 去重后只需要对 t 按 dfs 序排序后按原图祖先关系建边即可。即 LCA(ti,ti+1)ti+1

为什么可以做到不重不漏?

证明
如果 x 是 y 的祖先,那么 x 直接到 y 连边。因为 dfs 序保证了 xy 的 dfs 序是相邻的,所以 xy 的路径上面没有关键点。
如果 x 不是 y 的祖先,那么就把 LCA 当作 y 的的祖先,根据上一种情况也可以证明 LCAy 点的路径上不会有关键点。
所以连接 LCAy,不会遗漏,也不会重复。

因为两个点才会产生一个 LCA,所以时间复杂度是 O(mlogn) 的。

虚树的建立:

il bool cmp(int a,int b){ 
	return id[a]<id[b]; 
} 
il void build(){ 
	t[++num]=1; 
	sort(a+1,a+k+1,cmp); 
	for(int i=1;i<k;i++){ 
		t[++num]=a[i]; 
		t[++num]=LCA(a[i],a[i+1]); 
	} t[++num]=a[k]; 
	sort(t+1,t+num+1,cmp); 
	num=unique(t+1,t+num+1)-t-1; 
	for(int i=1;i<num;i++) vec[LCA(t[i],t[i+1])].push_back(t[i+1]); 
} 

P2495 [SDOI2011] 消耗战

板子题。

考虑暴力:设 dpu 表示使 u 子树内所有关键点都与 1 断开的最小代价,令 Minu 表示 1u 的路径上边权最小值。

fu={Minu[u]min(Minu,fto)[u]

虚树建出来就可以了。

dp 部分代码:

il void dfs(int u){ 
	dp[u]=0;  
	for(auto to:vec[u]){ 
		dfs(to); 
		dp[u]+=dp[to]; 
	} if(st[u]) dp[u]=Min[u]; 
	else dp[u]=min(dp[u],1ll*Min[u]); 
	vec[u].clear(); st[u]=false; 
} 

P6572 [BalticOI 2017] Railway

蠢猪题。

建立出虚树后树上差分一下即可,唯一值得注意的点是 1 有可能不在要算贡献里,但虚树中又不得不以 1 做根,所以需要在差分算贡献时特判一下。

il void dfs(int u){ 
	for(auto to:vec[u]){ 
		if(u!=1||flg) dp[to]++,dp[u]--; 
		dfs(to); 
	} st[u]=false,vec[u].clear(); 
} 

P4103 [HEOI2014] 大工程

先考虑最小最大值。

定义 MinuMaxu 表示 u 子树内所有关键点到 u 的最小/最大值。

答案有两种情况:

  1. u 是关键点,在儿子中找到最小/最大值。

  2. u 不是关键点,找两个儿子的值拼起来即可。

再考虑代价和,其实就是计算每一条边对答案的贡献。

定义 dpu 表示 u 子树内所有关键点到 u 的距离和,gu 表示 u 子树内关键点个数。

uto 这一段路径的长度记为 wto 儿子的贡献即为 (gugto)×(dpto+gto×w),即就是除开 to 儿子后其他关键点和 to 子树连边的次数乘上 to 子树内所有关键点到 u 的距离和。

建出虚树即可。dp 代码:

il void dfs(int u){ 
	dp[u]=g[u]=0; 
	Mindep[u]=INF,Maxdep[u]=-INF; 
	if(st[u]) g[u]=1,Mindep[u]=Maxdep[u]=0; 
	for(auto to:vec[u]){ 
		dfs(to); 
		int w=dep[to]-dep[u]; 
		Min=min(Min,Mindep[u]+Mindep[to]+w); 
		Max=max(Max,Maxdep[u]+Maxdep[to]+w); 
		Mindep[u]=min(Mindep[u],Mindep[to]+w); 
		Maxdep[u]=max(Maxdep[u],Maxdep[to]+w); 
		g[u]+=g[to],dp[u]+=dp[to]+w*g[to]; 
	} for(auto to:vec[u]){ 
		int w=dep[to]-dep[u]; 
		ans+=1ll*(g[u]-g[to])*(dp[to]+w*g[to]); 
	} st[u]=false,vec[u].clear(); 
} 

CF613D Kingdom and its Cities

先考虑无解的情况:一个点是关键点且他的父亲也是关键点。

建立出虚树后,还是分两种情况讨论:

  1. u 是关键点,则需要将它与儿子节点的路径断掉。

  2. u 不是关键点,记 cnt 为它的儿子节点中关键点的数量,若 cnt>1,则将 u 占领。若 cnt=1,则将 u 标记为关键点,回溯到父节点去短边。

dp 代码:

il int dfs(int u){ 
	int res=0,cnt=0; 
	for(auto to:vec[u]) res+=dfs(to),cnt+=(st[to]?1:0); 
	if(st[u]) res+=cnt; 
	else{ 
		if(cnt>1) res++; 
		else if(cnt==1) st[u]=true; 
	} return res; 
} 

P3233 [HNOI2014] 世界树

毒瘤题。思路来自这里

不妨先建立出虚树,答案分为两部分求解:

定义 gu 表示离 u 最近的关键点。

显然通过两个 dfs 去更新:第一个 dfs 计算儿子节点中的关键点,第二个 dfs 计算父亲节点的关键点。

关键点子树内的贡献

然后我们可以更新出 u 的儿子子树中不含关键点的儿子子树的贡献,即为 sizusizsonsonu 虚树上的儿子这个方向的直接儿子。

两个关键点间路径和及其字数内的节点

对于虚树上的点 u,to 的路径中的点的贡献,此时可以分为两种情况:

  1. gu=gto,显然这条路径上的点都会为 gu 做贡献。

  2. 二分出断点 p,qp 及上半部分属于 guq 及下半部分属于 gto,大力分讨即可。

il void dfs1(int u){ 
	g[u]=-1; 
	if(st[u]) g[u]=u; 
	for(auto to:vec[u]){ 
		dfs1(to); 
		if(g[to]==-1) continue; 
		if(g[u]==-1) g[u]=g[to]; 
		else{ 
			int d1=get(u,g[u]),d2=get(u,g[to]); 
			if(d2<d1||(d1==d2&&g[to]<g[u])) g[u]=g[to]; 
		} 
	} 
} 
il void dfs2(int u){ 
	for(auto to:vec[u]){ 
		if(g[to]==-1) g[to]=g[u]; 
		else{ 
			int d1=get(to,g[to]),d2=get(to,g[u]); 
			if(d2<d1||(d1==d2&&g[u]<g[to])) g[to]=g[u]; 
		} dfs2(to); 
	} 
} 
il void calc(int u){ 
	ans[g[u]]+=siz[u]; 
	for(auto to:vec[u]){ 
		int w=dep[to]-dep[u]-1; 
		// u 没有关键点的子树的贡献 :
		ans[g[u]]-=siz[plc(to,w)]; 
		// 剩下的情况分类讨论 :
		if(g[u]==g[to]) ans[g[u]]+=siz[plc(to,w)]-siz[to]; 
		else{ 
			int d1=get(u,g[u]),d2=get(to,g[to]); 
			int l=0,r=w,res; 
			while(l<=r){ 
				int mid=l+r>>1; 
				if((d1+mid<d2+w+1-mid)||(d1+mid==d2+w+1-mid&&g[u]<g[to])) l=mid+1,res=mid; 
				else r=mid-1; 
			} ans[g[u]]+=siz[plc(to,w)]-siz[plc(to,w-res)]; 
			ans[g[to]]+=siz[plc(to,w-res)]-siz[to];  
		} calc(to); 
	} 
} 
posted @   Celestial_cyan  阅读(25)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
点击右上角即可分享
微信分享提示