虚树 学习笔记

虚树 Virtual Tree 学习笔记

引入

P2495 [SDOI2011] 消耗战

题目大意:给一棵 \(n\) 个点的树,\(m\) 次询问 \(k\) 个点,要求切断一些边使点 1 不可达这些点,求最小切断的边权和。

\(n\le 2.5*10^5,m\le 5*10^5,\sum k\le 5*10^5\)


先考虑一个朴素的 DP,每次询问扫一遍整个树。

\(f_i\) 为使点 \(i\) 的子树内的关键点不与 1 连通的最小花费,

\(g_i\) 为 1 到 \(i\) 路径上的边权的最小值。

\(i\) 为查询的点,\(f_i=g_i\)

否则 \(f_i=\min(g_i,\sum f_{son_i})\)

时间复杂度 \(O(nQ)\)


发现 \(\sum k\)\(n\) 同阶,所以一次查询里的关键点很少,很多点都是没有用的。

这时可以用虚树来优化。

什么是虚树?

虚树常见于树形 DP 中。

如果一次询问所包含的关键点很少(关键点可以理解为对答案有实际影响的点),即有很多点都是多余无用的。

如果每次我们还是暴力地扫一遍整棵树,那是 \(O(nQ)\) 的。

我们考虑把关键点及其有关的点取出来,按照原树的祖先后代关系新建一棵树,这就是虚树。

如果所有询问的关键点总数与 \(n\) 同阶,则最后复杂度就是 \(O(n+Q)\) 了。

如何建一棵虚树?

前置知识

我们需要一个高效的 LCA 算法;

\(O(\log n)\) 可以用常数更小的树剖代替倍增。

\(O(n\log n)\) 预处理,\(O(1)\) 查询的 RMQ 也可。


选点

显然我们需要的点是那些关键点和他们之间的 LCA。

根据前人的智慧加上我自己的感性理解,先把所有关键点按 dfs 序排好然后相邻两个求 LCA。

这些 LCA + 根节点 + 所有 \(k\) 个关键点 = 我们所需的点。

所有点的个数就是 \(O(k)\) 的。

连边

先把所有点按 dfs 序排序,然后去重。

我们用一个单调栈维护虚树上由根节点出发的一条链。

开始把根节点 1 入栈。

之后遍历排好序的点。

设当前点为 \(x=d_i\) 与栈顶为点 \(top\)

\(LCA(x,top)=top\),则点 \(x\) 仍在 \(1\rightarrow top \rightarrow x\) 这条链上,则连边 \((top,x)\),将 \(x\) 直接入栈。

\(y=LCA(x,top)\neq top\),则点 \(x\) 不在 \(1\rightarrow top\) 这条链上了,\(y\) 即为这条链与链 \(1\rightarrow x\) 的交叉点。

\(y\) 显然也是我们选的点之一,此时一直退栈直到栈顶为 \(y\),然后连边 \((y,x)\),将 \(x\) 入栈。

回顾连边

\(d_i\) 连完边后,事实上我们的栈顶一直都为 \(d_i\)

而与 \(d_i\) 连边的点为栈顶 \(LCA(top,d_i)\),即 \(LCA(d_{i-1},d_i)\)

也就是说在排完序后,只需连 \((LCA(d_{i-1},d_i),d_i)\) 即可,看起来十分简洁,其中要连 \((1,d_1)\)

也就是说我们实际并不需要一个栈来维护链。

总结

  1. 把关键点按 dfs 序排序。
  2. 关键点相邻两个求 LCA。
  3. 所有点(LCA + 关键点)按 dfs 序排序。
  4. 遍历数组,连 \((LCA(d_{i-1},d_i),d_i)\)\((1,d_1)\)

建完虚树后再在虚树上做开头的 DP 即可。

于是我们的第一道题就迎刃而解了!

代码如下:

#include<bits/stdc++.h>
using namespace std;
int n,m;
const int N=2.5e5+5;
int to[N*2],nx[N*2],st[N],tot,co[N*2];
void add(int u,int v,int w){
	to[++tot]=v,nx[tot]=st[u],st[u]=tot,co[tot]=w;
}
vector<int> edge[N];
int top[N],fa[N],son[N],sz[N],d[2*N],dep[N];
#define ll long long 
ll f[N],g[N];
int lca(int x,int y){
	while(top[x]!=top[y]){
		if(dep[top[x]]>dep[top[y]])x=fa[top[x]];
		else y=fa[top[y]];
	}
	if(dep[x]>dep[y])return y;
	return x;
}
int dfn[N],dfn1;
void dfs(int x,int y){
	sz[x]=1,fa[x]=y,dep[x]=dep[y]+1;
	for(int i=st[x];i;i=nx[i]){
		int v=to[i];
		if(v!=y){
			g[v]=min(g[x],(ll)co[i]);
			dfs(v,x),sz[x]+=sz[v];
			if(sz[v]>sz[son[x]])son[x]=v;
		}
	}
}
void dfs2(int x){
	dfn[x]=++dfn1;
	if(son[fa[x]]==x)top[x]=top[fa[x]];
	else top[x]=x;
	if(son[x])dfs2(son[x]);
	for(int i=st[x];i;i=nx[i]){
		int v=to[i];
		if(v!=fa[x]&&v!=son[x])dfs2(v);
	}
}
int K;
int cmp(int x,int y){
	return dfn[x]<dfn[y];
}
int bz[N];
void solve(int x){
	ll sum=0;
	for(int v:edge[x]){
		solve(v);
		sum+=f[v];
	}
	edge[x].clear();
	if(bz[x])f[x]=g[x];
	else f[x]=min(g[x],sum);
	bz[x]=0;
}
int main(){
//	freopen("1.in","r",stdin);
	ios::sync_with_stdio(0);
	cin>>n;
	for(int i=1;i<n;i++){
		int u,v,w;
		cin>>u>>v>>w;
		add(u,v,w),add(v,u,w);
	}
	g[1]=1e18;
	dfs(1,0);
	dfs2(1);
	cin>>m;
	while(m--){
		cin>>K;
		for(int i=1;i<=K;i++){
			cin>>d[i];
			bz[d[i]]=1;
		}
		sort(d+1,d+1+K,cmp);
		int tt=0;
		for(int i=1;i<K;i++){
			d[i+K]=lca(d[i],d[i+1]);
		}
		d[K+K]=1;
		sort(d+1,d+K+K+1,cmp);
		for(int i=1;i<=K+K;i++){
			if(d[i-1]!=d[i])d[++tt]=d[i];
		}
		for(int i=2;i<=tt;i++){
			edge[lca(d[i-1],d[i])].push_back(d[i]);
		}
		solve(1); 
		cout<<f[1]<<"\n";
	}
}

套路

数据范围中看见类似 \(\sum m\le 10^5\) 的东西时就要考虑建虚树了。

但是建虚树人人都会,重点是如何计算。

例题

posted @ 2024-09-26 21:39  dengchengyu  阅读(7)  评论(0编辑  收藏  举报