虚树简介
有一类问题是形如:针对一棵 个点的树有 次询问,询问的东西可以在原来的树上轻松完成,但是会 TLE,并且询问只和一些关键点有关,这时可以考虑建出一棵浓缩了所有原树的关键点信息的、规模为 ( 为关键点个数)的虚树,在虚树上可以容易地处理询问,一般在保证 与 同阶情况下高效处理了询问。
建立虚树的方法
这颗树上有 个关键点,它们两两之间的 LCA 不同的总共不超过 个,即为将它们按 dfn 排序后(环状)相邻的两个之间的 LCA 们。
虚树就是关键点、LCA 集合、根的并,按原树祖先后代关系重建出的一棵树。不难发现数的规模 。
普遍的建虚树的方法是使用 dfn 单调栈(其实就是一个栈而已),步骤如下:
- 将关键点按 dfn 递增顺序排序。
- 将根节点入栈。
- 开一个 vector 存储虚树中的点(因为我们现在还不知道它们)。
- 从根节点开始 dfs 这棵树,对于遇到的关键点 ,依次执行下面的步骤:
- 将栈顶和 取 LCA,记作 。显然栈不为空。
- 如果栈顶的 dfn 大于 的 dfn,则依次执行下面的两个步骤:
- 一直弹栈,直到栈内只剩唯一元素或次栈顶的 dfn 不大于 的 dfn。每次弹栈将次栈顶和栈顶之间连边于虚树。
- 将 和栈顶连边于虚树,并弹栈。
- 如果栈顶 ,将 入栈。
- 将 入栈。
- 此时栈中应仍有元素,一直弹栈,直到栈空。若栈内有超过一个元素,则每次弹栈将次栈顶和栈顶之间连边于虚树。
- 清空:必须 清空,因此上述每次弹栈送入 vector,显然 vector 内元素互异,遍历并清空其邻接表从而清空虚树。
解释:
- 单调栈中保存的时刻是一条根节点到当前关键点的虚树中节点路径。
- 步骤 4.2 其实模拟的是下图所示情形:
- 虚树中的连边有时会依据题目情景而有边权,有时有向有时无向。
- 复杂度为 。
推荐题目
[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"); } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App