一.虚树的定义与作用.
虚树:在一棵 n n n个点的树上,基于某 k k k个点与它们两两之间LCA构造的一棵新树满足这 k k k个点之间的辈分关系与原树一样,这种树称为虚树.
可以证明虚树的节点个数不会超过 2 k − 1 2k-1 2k−1个.
虚树解决的问题一般会对一棵 n n n个点的树进行多次的询问,每次询问只会用到树上 k k k个点的信息,并且 k k k的总和不超过一个数值.
这类问题的一个特性是若每次询问都大力在原树上得出答案会使得复杂度与
n
n
n同级,但若构建出原树基于需要用到的
k
k
k个点构造的虚树上得到答案则会使得复杂度变成与
k
k
k同级,能够大大减少不必要的遍历节点.
二.虚树的构造.
现在我们来看虚树的构造.
在构造一棵虚树前,我们先对原树跑出每一个点的dfs序,然后把 k k k个点按照对应dfs序大小排个序.
排完序后,我们枚举这 k k k个点.假设我们现在枚举到了第 i i i个点,并且用一个栈维护了第 i − 1 i-1 i−1个点到原树根上所有的在虚树上存在的点,并构造出了前 i − 1 i-1 i−1个点的虚树.
现在我们考虑如何构造前 i i i个点的虚树,显然我们最多只会增加两个点,一个是第 i i i个点本身,另一个第 i i i个点与第 i − 1 i-1 i−1个点的LCA,并且我们要同时把维护链的栈更新好.
这个时候我们考虑求出第 i i i个点与第 i − 1 i-1 i−1个点的LCA,先把栈中这个LCA的所有后代去掉,并判定这个LCA是否在栈中,若不在则加入栈中,然后在栈中加入第 i i i个点.
至于连边,我们需要在每一次去掉一个点的时候才能连这个点与它父亲之间边.
这样我们就可以完成构造啦.
至于连边的边权什么的就要看具体的题目了.
设要多次建立虚树总点数为 k k k,构造虚树的时间复杂度为 O ( k log n ) O(k\log n) O(klogn).
还有一点,建虚树的时候一般可以把根也放到关键点中.
代码如下:
//假设虚树上的边权是原树上这条链的边权最小值
//tag为1表示这个点时原来的关键点,为0表示是关键点的LCA
int ca,a[N+9],lca[N+9],tag[N+9];
int sta[N+9],cst;
bool cmp(const int &a,const int &b){return dfs[a]<dfs[b];}
void Build(){
sort(a+1,a+ca+1,cmp);
sta[cst=1]=a[0]=1;
for (int i=1;i<=ca;++i){
lca[i]=Query_lca(a[i],a[i-1]);
lin[1][a[i]]=lin[1][lca[i]]=tag[lca[i]]=0;
}
for (int i=1;i<=ca;++i) tag[a[i]]=1;
for (int i=1;i<=ca;++i){
for (;cst>1&&dep[lca[i]]<dep[sta[cst-1]];--cst)
Ins(1,sta[cst-1],sta[cst],Query_min(sta[cst],sta[cst-1]));
if (lca[i]^sta[cst]){
Ins(1,lca[i],sta[cst],Query_min(sta[cst],lca[i]));
if (lca[i]^sta[--cst]) sta[++cst]=lca[i];
}
if (a[i]^sta[cst]) sta[++cst]=a[i];
}
for (;cst>1;--cst) Ins(1,sta[cst-1],sta[cst],Query_min(sta[cst],sta[cst-1]));
}
更详细的代码参见BZOJ2286.
三.树链的并.
树链的并:在一棵 n n n个点的树上有 k k k个点 a i a_i ai,现在要求精确覆盖根到每个 a i a_i ai的链(也就是说每个在树链并上的点只能被覆盖一次).
这个问题其实很简单,用类似虚树构建的方式维护即可.
比如说我们现在要把 k k k个点 a i a_i ai为基础的树链的并上所有点点权 + 1 +1 +1,就可以先把 a i a_i ai按照dfs序排序,然后枚举每个点 a i a_i ai,把 a i a_i ai到 a i a_i ai与 a i − 1 a_{i-1} ai−1的LCA这条链都 + 1 +1 +1,大力用数据结构维护即可.
具体应用可以参见BZOJ3881.