虚树简介

有一类问题是形如:针对一棵 n 个点的树有 q 次询问,询问的东西可以在原来的树上轻松完成,但是会 TLE,并且询问只和一些关键点有关,这时可以考虑建出一棵浓缩了所有原树的关键点信息的、规模为 O(k)k 为关键点个数)的虚树,在虚树上可以容易地处理询问,一般在保证 kn 同阶情况下高效处理了询问。

建立虚树的方法

这颗树上有 k 个关键点,它们两两之间的 LCA 不同的总共不超过 k 个,即为将它们按 dfn 排序后(环状)相邻的两个之间的 LCA 们。
虚树就是关键点、LCA 集合、根的并,按原树祖先后代关系重建出的一棵树。不难发现数的规模 2k+1

普遍的建虚树的方法是使用 dfn 单调栈(其实就是一个栈而已),步骤如下:

  1. 将关键点按 dfn 递增顺序排序。
  2. 将根节点入栈。
  3. 开一个 vector 存储虚树中的点(因为我们现在还不知道它们)。
  4. 从根节点开始 dfs 这棵树,对于遇到的关键点 h,依次执行下面的步骤:
    • 将栈顶和 h 取 LCA,记作 lca。显然栈不为空。
    • 如果栈顶的 dfn 大于 lca 的 dfn,则依次执行下面的两个步骤:
      • 一直弹栈,直到栈内只剩唯一元素或栈顶的 dfn 不大于 lca 的 dfn。每次弹栈将次栈顶和栈顶之间连边于虚树。
      • lca 和栈顶连边于虚树,并弹栈。
    • 如果栈顶 lca,将 lca 入栈。
    • h 入栈。
  5. 此时栈中应仍有元素,一直弹栈,直到栈空。若栈内有超过一个元素,则每次弹栈将次栈顶和栈顶之间连边于虚树。
  6. 清空:必须 O(k) 清空,因此上述每次弹栈送入 vector,显然 vector 内元素互异,遍历并清空其邻接表从而清空虚树。

解释:

  • 单调栈中保存的时刻是一条根节点到当前关键点的虚树中节点路径。
  • 步骤 4.2 其实模拟的是下图所示情形:
  • 虚树中的连边有时会依据题目情景而有边权,有时有向有时无向。
  • 复杂度为 O(klogn)

推荐题目

[SDOI2011]消耗战

很容易被题目提示联想到使用虚树。在虚树中进行简单的树形 dp 即可,边权设置为原树上两点之间的边权最小值,可以用 lca 顺带求。

代码的核心部分之一是求连通图的虚树
复制
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=5e5+5,INF=1e9;
int n,k,q,tp,dfc,h[N],dep[N],fa[N][20],mn[N][20],stk[N],dfn[N];
ll f[N];
bool imp[N];
vector<pair<int,int> >G[N],T[N];
inline int read(){
register char ch=getchar();register int x=0;
while(ch<'0'||ch>'9')ch=getchar();
while(ch>='0'&&ch<='9')x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return x;
}
void dfs1(int x,int p){
dep[x]=dep[p]+1,fa[x][0]=p,dfn[x]=++dfc;
for(int i=1;i<=19;i++){
fa[x][i]=fa[fa[x][i-1]][i-1];
mn[x][i]=min(mn[x][i-1],mn[fa[x][i-1]][i-1]);
}
for(int i=0;i<G[x].size();i++){
int y=G[x][i].first,z=G[x][i].second;
if(y^p){
mn[y][0]=z;
dfs1(y,x);
}
}
}
inline int glca(int u,int v){
if(u==v)return u;
if(dep[u]>dep[v])swap(u,v);
for(int i=19;~i;i--)if(dep[fa[v][i]]>=dep[u])v=fa[v][i];
if(u==v)return u;
for(int i=19;~i;i--)if(fa[u][i]!=fa[v][i])u=fa[u][i],v=fa[v][i];
return fa[u][0];
}
inline int gmin(int u,int v){
if(u==v)return 0;
if(dep[u]>dep[v])swap(u,v);
int s=1e9;
for(int i=19;~i;i--)if(dep[fa[v][i]]>=dep[u])s=min(s,mn[v][i]),v=fa[v][i];
if(u==v)return s;
for(int i=19;~i;i--)if(fa[u][i]!=fa[v][i])s=min(s,min(mn[u][i],mn[v][i])),u=fa[u][i],v=fa[v][i];
return min(s,min(mn[u][0],mn[v][0]));
}
void dfs2(int x){
f[x]=0;
if(imp[x]){f[x]=INF;return;}
for(int i=0;i<T[x].size();i++){
int y=T[x][i].first,z=T[x][i].second;
dfs2(y),f[x]+=min(f[y],(ll)z);
}
}
void init(){
for(int i=1;i<=k;i++)imp[h[i]]=1;
sort(h+1,h+k+1,[](int a,int b){return dfn[a]<dfn[b];});
stk[tp=1]=1;
vector<int>vec;
for(int i=1;i<=k;i++){
int lca=glca(stk[tp],h[i]);
if(tp>1&&dfn[stk[tp]]>dfn[lca]){
while(tp>1&&dfn[stk[tp-1]]>dfn[lca])T[stk[tp-1]].push_back(make_pair(stk[tp],gmin(stk[tp-1],stk[tp]))),vec.push_back(stk[tp--]);
vec.push_back(stk[tp--]);
T[lca].push_back(make_pair(stk[tp+1],gmin(stk[tp+1],lca)));
}
if(stk[tp]!=lca)stk[++tp]=lca;
stk[++tp]=h[i];
}
while(tp)T[stk[tp-1]].push_back(make_pair(stk[tp],gmin(stk[tp-1],stk[tp]))),vec.push_back(stk[tp--]);
dfs2(1);
cout<<f[1]<<'\n';
for(int i=0;i<vec.size();i++)T[vec[i]].clear();
for(int i=1;i<=k;i++)imp[h[i]]=0;
}
int main(){
n=read();
for(int i=1,u,v,w;i<n;i++){
u=read(),v=read(),w=read();
G[u].push_back(make_pair(v,w)),G[v].push_back(make_pair(u,w));
}
dfs1(1,0);
q=read();
while(q--){
k=read();
for(int i=1;i<=k;i++)h[i]=read();
init();
}
}

CF639F Bear and Chemistry

代码的核心部分之一是求非连通的虚树(虚森林)
/*
1. Build initial graph in init.G
2. Compress vertex via e-dcc and store it in init.T
3. For each query:
1) Select vital vertexes: V:x and E:u,v
2) Build virtual tree of forest init.T and store it in now.G
3) Connect edges in E in now.G
4) Run e-dcc again and update bel[], finally verify V
*/
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+5;
int n,m,q,k,n_v,n_e,R,tp,_rt,tfc,Edge,h[N*3],stk[N*2],tfn[N],fa[N][20],rt[N],dep[N];
vector<int>v;
vector<pair<int,int> >e;
inline int read(){
register char ch=getchar();register int x=0;
while(ch<'0'||ch>'9')ch=getchar();
while(ch>='0'&&ch<='9')x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return x;
}
inline int rotate(int element){
element=(element+R)%n;
if(!element)element=n;
return element;
}
struct Graph {
vector<pair<int,int> >G[N];
vector<int>T[N];
int dfc,tp,cnt,dfn[N],low[N],stk[N],bel[N];
vector<pair<int,int> >cut_e;
inline void cl(){dfc=tp=cnt=0;cut_e.clear();}
void Tarjan(int x,int pid,int p){//cout<<x;
dfn[x]=low[x]=++dfc,stk[++tp]=x;
for(int i=0;i<G[x].size();i++){
int y=G[x][i].first,z=G[x][i].second;
if(z^pid){
if(!dfn[y]){
Tarjan(y,z,x);
low[x]=min(low[x],low[y]);
}
else low[x]=min(low[x],dfn[y]);
}
}
if(dfn[x]==low[x]){
cnt++;
while(tp){
bel[stk[tp]]=cnt;
if(stk[tp--]==x)break;
}
if(p)cut_e.push_back(make_pair(p,x));
}
}
}init,now;
void dfs(int x,int p){
rt[x]=_rt;
tfn[x]=++tfc,fa[x][0]=p,dep[x]=dep[p]+1;
for(int i=1;i<=19;i++)fa[x][i]=fa[fa[x][i-1]][i-1];
for(int i=0;i<init.T[x].size();i++){
int y=init.T[x][i];
if(y^p)dfs(y,x);
}
}
inline int glca(int u,int v){
if(u==v)return u;
if(dep[u]>dep[v])swap(u,v);
for(int i=19;~i;i--)if(dep[fa[v][i]]>=dep[u])v=fa[v][i];
if(u==v)return u;
for(int i=19;~i;i--)if(fa[u][i]!=fa[v][i])u=fa[u][i],v=fa[v][i];
return fa[u][0];
}
inline void adde(int u,int v){if(!u||!v)return;now.G[u].push_back(make_pair(v,++Edge)),now.G[v].push_back(make_pair(u,Edge));}
bool build(){
vector<int>vec;
tp=0;
for(int i=1;i<=k;i++){
if(rt[h[i]]!=rt[h[i-1]]){
while(tp)adde(stk[tp-1],stk[tp]),vec.push_back(stk[tp--]);
stk[tp=1]=rt[h[i]];
}
int lca=glca(h[i],stk[tp]);
if(tp>1&&tfn[stk[tp]]>tfn[lca]){
while(tp>1&&tfn[stk[tp-1]]>tfn[lca])
adde(stk[tp-1],stk[tp]),vec.push_back(stk[tp--]);
adde(lca,stk[tp]),vec.push_back(stk[tp--]);
}
if(stk[tp]!=lca)stk[++tp]=lca;
if(lca!=h[i])stk[++tp]=h[i];
}
while(tp)adde(stk[tp-1],stk[tp]),vec.push_back(stk[tp--]);
for(int i=0;i<n_e;i++)adde(init.bel[e[i].first],init.bel[e[i].second]);
for(int i=0;i<vec.size();i++)if(!now.dfn[vec[i]])now.Tarjan(vec[i],0,0);
bool fl=1;
for(int i=1;i<n_v;i++)if(now.bel[init.bel[v[i]]]!=now.bel[init.bel[v[i-1]]]){fl=0;break;}
for(int i=0;i<vec.size();i++)now.G[vec[i]].clear(),now.dfn[vec[i]]=0;
now.cl();
return fl;
}
signed main(){
n=read(),m=read(),q=read();
for(int i=1,u,v;i<=m;i++){
u=read(),v=read();
init.G[u].push_back(make_pair(v,++Edge)),init.G[v].push_back(make_pair(u,Edge));
}
for(int i=1;i<=n;i++)if(!init.dfn[i])init.Tarjan(i,0,0);
for(int i=0;i<init.cut_e.size();i++){
init.T[init.bel[init.cut_e[i].first]].push_back(init.bel[init.cut_e[i].second]);
init.T[init.bel[init.cut_e[i].second]].push_back(init.bel[init.cut_e[i].first]);
}
for(int i=1;i<=init.cnt;i++)if(!tfn[i])_rt=i,dfs(i,0);
for(int id=1;id<=q;id++){
n_v=read(),n_e=read();
v.resize(n_v),e.resize(n_e);
k=0;
for(int i=0;i<n_v;i++)v[i]=rotate(read()),h[++k]=init.bel[v[i]];
for(int i=0;i<n_e;i++){
e[i].first=rotate(read()),e[i].second=rotate(read());
h[++k]=init.bel[e[i].first],h[++k]=init.bel[e[i].second];
}
sort(h+1,h+k+1,[](int a,int b){return tfn[a]<tfn[b];});
k=unique(h+1,h+k+1)-h-1;
if(build())puts("YES"),R+=id;
else puts("NO");
}
}
posted @   pengyule  阅读(145)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App
点击右上角即可分享
微信分享提示