虚树学习笔记

虚树简介

详见 OI-Wiki

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

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

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

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

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

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

因为两个点才会产生一个 \(\operatorname{LCA}\),所以时间复杂度是 \(O(m\log n)\) 的。

虚树的建立:

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] 消耗战

板子题。

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

\[ f_u=\left\{\begin{matrix} Min_u & [u 是关键点]\\ \min(Min_u,\sum f_{to}) & [u 不是关键点] \end{matrix}\right. \]

虚树建出来就可以了。

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] 大工程

先考虑最小最大值。

定义 \(Min_u\)\(Max_u\) 表示 \(u\) 子树内所有关键点到 \(u\) 的最小/最大值。

答案有两种情况:

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

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

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

定义 \(dp_u\) 表示 \(u\) 子树内所有关键点到 \(u\) 的距离和,\(g_u\) 表示 \(u\) 子树内关键点个数。

\(u\)\(to\) 这一段路径的长度记为 \(w\)\(to\) 儿子的贡献即为 \((g_u-g_{to}) \times (dp_{to}+g_{to} \times 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] 世界树

毒瘤题。思路来自这里

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

定义 \(g_u\) 表示离 \(u\) 最近的关键点。

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

关键点子树内的贡献

然后我们可以更新出 \(u\) 的儿子子树中不含关键点的儿子子树的贡献,即为 \(siz_u-\sum siz_{son}\)\(son\)\(u\) 虚树上的儿子这个方向的直接儿子。

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

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

  1. \(g_u=g_{to}\),显然这条路径上的点都会为 \(g_u\) 做贡献。

  2. 二分出断点 \(p,q\)\(p\) 及上半部分属于 \(g_u\)\(q\) 及下半部分属于 \(g_{to}\),大力分讨即可。

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 @ 2024-04-08 19:23  Celestial_cyan  阅读(46)  评论(0)    收藏  举报