给定一棵背景树,再给定这棵树上的一个点集,那么显然,这个点集里的所有点并上两两 LCA,然后按祖先后代关系连边得到的一棵小树可以保证这个点集里的相对关系。这棵树叫做这个点集生成的虚树。
我们将这个点集按欧拉序排序。那么它们的 LCA 集合显然是所有相邻对的 LCA 的集合,由欧拉序 + ST 表求 LCA 法可知。那么虚树大小是和点集大小同阶的。
给定大小为 (m) 的点集,我们有 (mathrm O(mlog m)) 的方法求出这个虚树。
我们考虑往集合里加一个根,这样更容易求虚树。一般来说带个根是不影响正确性的,必要时可以手动删除。
我们考虑按照这些点的 dfn 从小到大(dfn 相对大小和欧拉序一样)模拟 dfs 的过程。那么实时维护一个递归栈(栈中都是虚树里的点)。
考虑按 dfn 依次加入点集里的点。考虑任意一步时,栈顶显然是按 dfn 排序的上一个。求出栈顶和当前节点的 LCA(每一步都求 LCA 的话,显然可以充满虚树,这个算法做的只是连边……),分两种情况:
- LCA 等于栈顶。那说明当前节点是栈顶的后代,直接加入递归栈;
- 不等于。那么感性理解可以得到,栈中 LCA 的后代的连边关系已经确定了,就一边弹一边连(注意最靠近 LCA 的那个是 LCA 连向它)。然后如果 LCA 不在栈中的话,就加入。然后再把当前节点加入栈。
最后一边清空栈一边连边。就做完啦。
给份代码 qwq:
bool cmp(int x,int y){return dfn[x]<dfn[y];}
int stk[N],top;
vector<int> son[N+1];
void virtree(vector<int> &v){
sort(v.begin(),v.end(),cmp);
stk[top++]=1;
for(int i=0;i<v.size();i++){
int _lca=lca(stk[top-1],v[i]);
if(_lca!=stk[top-1]){
while(dfn[_lca]<dfn[stk[top-2]])son[stk[top-2]].pb(stk[top-1]),top--;
son[_lca].pb(stk[top-1]),top--;
if(_lca!=stk[top-1])stk[top++]=_lca;
}
stk[top++]=v[i];
}
while(top>=2)son[stk[top-2]].pb(stk[top-1]),top--;
top--;
}
建出虚树之后,可以解决这样的问题:给若干个询问,每个询问给一个树上的点集叫你搞事情。不保证点集大小和询问个数,但是保证 (sum m)。一般就建虚树来搞啦,几乎跟在原树上搞一样,可以 DP 啊淀粉质啊啥的。但是有的时候边变成了原树上的直链,需要直链查询啥的,或者有其它的小变化?总之虚树的题的虚树部分都很板子就对啦 qwq。