虚树

虚树的概念

虚树,是对于一棵给定的节点数为 \(n\) 的树 \(T\),构造一棵新的树 \(T\) 使得总结点数最小且包含指定的某几个节点和他们的LCA。

虚树解决的问题

利用虚树,可以对于指定多组点集 \(S\) 的询问进行每组 \(O(\lvert S\rvert(\log n+\log\lvert S\rvert)+f(\lvert S\rvert))\) 的回答,其中 \(f(x)\) 指的是对于一棵 \(x\) 个点的树单组询问这个问题的时间复杂度。可以看到,这个复杂度基本上(除了那个 \(\log n\) 以外)与 \(n\) 无关了。这样,对于多组询问的回答就可以省去每次询问都遍历一整棵树的 \(O(n)\) 复杂度了。

虚树的构建

二次排序法

for(int i=1;i<=m;i++){cin>>b[i];vis[b[i]]=1;}
sort(b+1,b+m+1,[](int x,int y){return dfn[x]<dfn[y];});
for(int i=1;i<tmp;i++) b[++m]=lca(b[i],b[i+1]);
sort(b+1,b+m+1,[](int x,int y){return dfn[x]<dfn[y];}),m=unique(b+1,b+m+1)-b-1;
for(int i=2;i<=m;i++) ne[lca(b[i-1],b[i])].push_back(b[i]);

例题

设 \(f_u\) 为 \(u\) 子树内的答案,\(g_u\) 表示 \(u\) 是否与关键点相连

如果 \(u\) 为关键点,则 \(f_u= \sum_{v \in son(u)} f_v+g_v\)

如果 \(u\) 不为关键点,如果 \(u\) 内有多个关键点,则将 \(u\) 删去,否则留下,看祖先能否一次分开

显然在关键点的lca出删除是不劣的,因此可以构建虚树

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define pii pair<int,int>
const int N=1e5+5;
vector<int> e[N],e1[N];
int siz[N],son[N],top[N],fa[N],dep[N],dfn[N],tot,f[N],g[N],vis[N],flag,n,q,b[N<<1];
void dfs(int u){
	dep[u]=dep[fa[u]]+1,siz[u]=1;
	for(auto v:e[u]){
		if(v==fa[u]) continue;
		fa[v]=u,dfs(v),siz[u]+=siz[v];
		if(siz[v]>siz[son[u]]) son[u]=v;
	}
}
void dfs2(int u,int t){
	top[u]=t,dfn[u]=++tot;
	if(son[u]) dfs2(son[u],t);
	for(auto v:e[u]) if(v!=son[u]&&v!=fa[u]) dfs2(v,v);
}
int lca(int x,int y){
	while(top[x]!=top[y]) dep[top[x]]<=dep[top[y]]?y=fa[top[y]]:x=fa[top[x]];
	
	return dep[x]<=dep[y]?x:y;
}
void solve(int u){
	if(flag) return;
	int sum=0;f[u]=g[u]=0;
	for(auto v:e1[u]){
		if(dep[u]+1==dep[v]&&vis[u]&&vis[v]){flag=1;return;}
		solve(v),sum+=g[v],f[u]+=f[v];
	}
	if(vis[u]) f[u]+=sum,g[u]=1;
	else if(sum==1) g[u]=1;
	else if(sum>1) f[u]++,g[u]=0;
	else g[u]=0;
}
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i=1;i<n;i++){int x,y;cin>>x>>y;e[x].push_back(y),e[y].push_back(x);}
	dfs(1),dfs2(1,1);
	cin>>q;
	while(q--){
		int m;cin>>m;int tmp=m;flag=0;
		for(int i=1;i<=m;i++) {cin>>b[i];vis[b[i]]=1;}
		sort(b+1,b+m+1,[](int x,int y){return dfn[x]<dfn[y];});
		for(int i=2;i<=tmp;i++) b[++m]=lca(b[i-1],b[i]);
		sort(b+1,b+m+1,[](int x,int y){return dfn[x]<dfn[y];});
		m=unique(b+1,b+m+1)-b-1;
		for(int i=2;i<=m;i++) e1[lca(b[i-1],b[i])].push_back(b[i]);
		solve(b[1]);
		cout<<(flag?-1:f[b[1]])<<"\n";
		for(int i=1;i<=m;i++) vis[b[i]]=0,e1[b[i]].clear();
	}
	return 0;
}

例2

与上面类似,同样分类讨论:

  1. 当前节点为重要节点:一定要删除到根的一条边
  2. 当前节点不为重要节点:可以删到根的一条边,也可以只删儿子

删除到根的一条边在虚树上看似是不可做的,但是我们可以贪心的减最小的边,可以证明最小方案会被算到

例3

虚树上的点是好处理的,难点在于怎么处理非虚树上的点

首先我们可以预处理出虚树上每个节点最近的关键节点,考虑处理 \(u \to v\) 边上的答案:

  1. \(u,v\) 最近点在边的同一侧:那么整条边上的所有点的最近点一定与 \(u,v\) 相同
  2. \(u,v\) 最近点不在同一侧:那么二分即可

​

posted @ 2026-10-30 14:52  Rainesxiao  阅读(4)  评论(0)    收藏  举报