虚树 学习笔记

虚树

定义

虚树就是一颗只包含一部分关键结点的树。在树形 dp 时,如果一次询问只需要涉及一部分节点,而把整棵树跑一遍时间复杂度过大,所以我们可以建立虚树,将非关键点构成的链简化成边或者减去,然后在虚树上进行 dp。其实就是一种树形 dp 优化时间复杂度的方法。

建立虚树

预处理原树的 dfs 序,LCA 相关。

维护一个最右链,top 为栈顶位置。将所有询问点按 dfs 序排序,然后依次放入栈中,判断连续两个询问点的位置关系。注意这个最右链是一条分界线,左侧的虚树已经完成构建。还需要注意的是这里的 top 是链的底端,整条链是从下向上存入栈中的。

然后将所有询问点顺次加入,设 \(now\) 为当前询问点, \(lc\) 为该点和栈顶点的 lca 即 \(lc=lca(st_{top},now)\).

\(lc\) 必然在我们维护的最右链上,这时考虑 \(lc\)\(st_{top}\),\(st_{top-1}\) 的关系。

如果 \(lc=st_{top}\),那么 \(now\)\(st_{top}\) 的子树中,直接把 \(now\) 入栈即可。

如果 \(lc\)\(st_{top}\)\(st_{top-1}\) 之间,此时最右链的末端从 \(st_{top-1}\rightarrow st_{top}\) 变成了 \(st_{top-1} \rightarrow lc \rightarrow st_{top}\)。我们需要把边 \(lc-st_{top}\) 加入虚树,然后把 \(st_{top}\) 出栈,把 \(lc\)\(now\) 入栈。

如果 \(lc=st_{top-1}\),此时和第二中情况大同小异,只是不需要把 \(lc\) 加入栈内了。

如果 \(dep_{lc} < dep_{st_{top-1}}\),此时 \(lc\) 不在 \(st_{top-1}\) 的子树中,最右链从 \(st_{top-3} \rightarrow st_{top-2} \rightarrow st_{top-1} \rightarrow st_{top}\) 变成了 \(st_{top-3} \rightarrow lc \rightarrow now\),我们需要使用循环讲最右链的末端依次剪下,将被剪下的边加入虚树,直到不在是这种情况,这时一定可以转化成前三种情况进行处理。

当最后一个询问点加入之后,再将最右链加入虚树,即可完成构建。

至于清空虚树,如果直接对图清空,时间复杂度太大无法承受,所以在 dfs 的过程中每访问一个结点清空即可。

例题 [SDOI2011] 消耗战

点击查看代码
/*令sum[i]表示切断i的子树中所有询问点的最小代价之和
再令m[i]表示i到1号点的路径中最小的边权
sum[i]=sigma(min(mi[v],sum[v])
解释一下:我们枚举i的所有子树,为了切断它的子树v
要么就直接把v断掉,此时的代价就是mi[v]
要么就把v里所有的询问点断掉,代价为sum[v]

考虑建立虚树进行优化
把询问点按照dfs序进行排序,相邻的点求lca
虚树其实不需要建树,只需要栈+欧拉序 
*/
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=250010;
struct edge{
	int to,nxt;
	ll val;
}e[2*maxn];

int head[maxn],edgenum;
void add_edge(int u,int v,ll w){
	e[++edgenum].nxt=head[u];
	e[edgenum].to=v;
	e[edgenum].val=w;
	head[u]=edgenum;
}

int fa[22][maxn];
int dfn,in[maxn],out[maxn],dep[maxn];
ll mi[maxn];
void dfs(int x){//预处理倍增和欧拉序
	in[x]=++dfn;
	for(int i=1;fa[i-1][x];i++){
		fa[i][x]=fa[i-1][fa[i-1][x]];
	}
	for(int i=head[x];i;i=e[i].nxt){
		int v=e[i].to;
		ll val=e[i].val;
		if(in[v]==0){
			dep[v]=dep[x]+1;
			mi[v]=min(mi[x],val);
			fa[0][v]=x;
			dfs(v);
		}
	}
	out[x]=++dfn;
	return;
}
int lca(int x,int y){
	if(dep[x]<dep[y])
		swap(x,y);
	int del=dep[x]-dep[y];
	for(int i=0;del;del>>=1,i++){
		if(del&1)
			x=fa[i][x];
	} 
	if(x==y)
		return x;
		//? 
	for(int i=20;i>=0;i--){
		if(fa[i][x]!=fa[i][y]){
			x=fa[i][x];
			y=fa[i][y];
		}
	}
	return fa[0][x];
}
bool cmp(int x,int y){
	int k1=(x>0)?in[x]:out[-x];
	int k2=(y>0)?in[y]:out[-y];
	return k1<k2;
}
int tr[4*maxn],m,n;
stack <int> s;
ll sum[maxn];
bool vis[maxn];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n;
	ll w;
	for(int i=1,x,y;i<n;i++){
		cin>>x>>y>>w;
		add_edge(x,y,w);
		add_edge(y,x,w);
	}
	mi[1]=0x7f7f7f7f;
	dfs(1);
	cin>>m;
	for(int i=1,cot;i<=m;i++){//对每一次询问分别建立虚树处理 
		cin>>cot;
		for(int j=1;j<=cot;j++){
			cin>>tr[j];
			vis[tr[j]]=true;//是否在虚树里 
			sum[tr[j]]=mi[tr[j]];//先处理询问点(也就是虚树的叶子节点 
		}
		sort(tr+1,tr+cot+1,cmp);
		for(int j=1;j<cot;j++){
			int lc=lca(tr[j],tr[j+1]);
			if(!vis[lc]){
				tr[++cot]=lc;
				vis[lc]=true;
			}
		}
		int tt=cot;
		for(int j=1;j<=tt;j++){
			tr[++cot]=-tr[j];//加入一个负的弹栈点 
		}
		if(!vis[1]){
			tr[++cot]=1;
			tr[++cot]=-1;
		} 
		sort(tr+1,tr+cot+1,cmp);
		for(int j=1;j<=cot;j++){
			if(tr[j]>0){
				s.push(tr[j]);//入栈点,入栈 
			}
			else{
				int qwq=s.top();
				s.pop();//pop掉剩下的栈顶之后就是它的父亲
				if(qwq!=1){
					int ff=s.top();
					sum[ff]+=min(sum[qwq],mi[qwq]);
				} 
				else{
					cout<<sum[1]<<endl;
				}
				sum[qwq]=0;
				vis[qwq]=false;
			} 
		}
	}
	return 0;
}
posted @ 2025-03-18 21:22  Aapwp  阅读(36)  评论(0)    收藏  举报
我给你一个没有信仰的人的忠诚