虚树 学习笔记
虚树
定义
虚树就是一颗只包含一部分关键结点的树。在树形 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;
}