全局平衡二叉树
全局平衡二叉树
P.S.早就想学了,一直拖到现在,名副其实的树论大杀器。
模板:
一道集训时的题目:P4211 [LNOI2014] LCA。
题意简述:给一棵树,给定\(l,r,x\),求\(\sum_{i=l}^{r}dep[\text{LCA}(i,x)]\),不强制在线。
先看看直接的树剖/LCT解法。
发现题目要求的是区间\([l,r]\)的权值和,我们可以差分考虑。
将区间\([l,r]\)差分为\([1,l-1]\)和\([1,r]\),分别求出权值相减即可。
考虑将差分完的区间离线,于是问题变为给定\(x,y\),求出\(\sum_{i=1}^{y}dep[\text{LCA}(i,x)]\)。
对\(y\)进行扫描线,求出每个\(x\)对\(y\)的贡献,发现每次扫到下一个\(y\)时,对于一个\(x\),贡献累加了\(dep[\text{LCA}(x,y)]\)。
回想一种比较暴力的求解LCA的方法,将\(y\)到根染色,在\(x\)向上跳的过程中,第一个碰到的染色点就是两点的LCA。
再来想想这一题,发现我们要的贡献恰恰是LCA的深度,在\(x\)向上跳的过程中,遇到染色点统计答案即可。
我们得到了一种做法,但时间复杂度为\(O(n^2)\),十分不优秀,考虑优化这个过程。
考虑一个很妙的转化:将\(y\)到根路径上的点权加一,统计\(x\)时答案即为\(x\)到根路径上的点权和。
其实就是将\(dep(x)\)拆开转为求解路径上的和,在\(x\)向上跳的过程中会遇到\(x\)和\([1,y]\)的点的LCA,累加贡献即可。
题目变为树上路径加+树上路径和,可以用小常数\(O(n log^2n)\)的树剖或大常数\(O(nlogn)\)LCT解决。
看看时间复杂度,不禁令人深思,有没有一种数据结构,将树剖的小常数和\(LCT\)的理论优秀的时间复杂度相结合呢?
切入正题:我们不如将树剖和LCT结合,变为小常数的全局平衡二叉树。
- Q:什么是全局平衡二叉树?
A:来源于Yang Zhe大佬的论文:QTREE 解法的一些研究,字面意思就是在任意一棵子树内,这棵树都达到了平衡二叉树的结构(非严格),即任意子树的左(右)子树的节点总数小于等于其节点总数的一半。
这样,全局平衡二叉树的高度就为优秀的\(O(logn)\)。
- Q:全局平衡二叉树是怎么做到小常数\(O(nlogn)\)吊打树剖和
LCT的呢?
A:通常认为,全局平衡二叉树是将LCT静态化,强制变成一棵全局平衡的树。
我们先用树剖的方法将树剖为\(O(logn)\)条链,对于每条链,我们将链看成点,点与点之间用原树上的轻边相连,发现缩点后的竟然还是一棵树。
于是我们可以将链动一点手脚,将每个链上的点附上一个权值,再重构成一棵平衡的二叉树。
具体如下:将每个点的权值记为与它相连的轻儿子的子树大小和加一,再递归重心,二分中点建树,类似于平衡树。最后,这棵局部的平衡二叉树就建好了。
显然子树的形态不会影响以后的建树过程,于是我们递归每条链,对每条链都建树即可。
我们只有\(O(logn)\)条链,总点数为\(n\)。对于每条链只会执行一次建树过程,也就是每个点只会被执行一次,每次递归用二分建树是\(O(nlogn)\)的,所以建树的时间复杂度为\(O(nlogn)\)。
- Q:全局平衡二叉树如何执行像树剖/
LCT一样的"树上路径加+树上路径和"呢?
A:我们可以发现许多全局平衡二叉树的性质。前面提到,全局平衡二叉树的树高是\(O(logn)\)的。
其次,在全局平衡二叉树跳轻边等价于在树剖中跳轻边,在树剖中,我们每次对于一条重链,查询节点到重链上的根的信息,这恰好为全局平衡二叉树中一个点的左子树。
所以对于每次操作,维护左子树的信息即可。
所以代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N=5e4+10,M=N*2,mod=201314;
int head[N],ver[M],nxt[M],tot=1;
void add(int x,int y){ver[++tot]=y,nxt[tot]=head[x],head[x]=tot;}
int s[N],f[N],to[N],n,m,b[N],bs[N],lc[N],rc[N],fa[N],ss[N];
void dfs1(int x,int father){
s[x]=1,f[x]=father;
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(y==f[x])continue;
dfs1(y,x);
s[x]+=s[y];
if(s[y]>s[to[x]])to[x]=y;
}
}
int build(int L,int R){
int x=L,y=R;
while(y-x>1){//二分找中点
int mid=(x+y)>>1;
if(2*(bs[mid]-bs[L])<=bs[R]-bs[L])x=mid;
else y=mid;
}
int root=b[x];
ss[root]=R-L;
if(L<x)lc[root]=build(L,x),fa[lc[root]]=root;
if(R>x+1)rc[root]=build(x+1,R),fa[rc[root]]=root;
return root;
}
int dfs2(int x){
for(int p=x;p;p=to[p]){
for(int i=head[p];i;i=nxt[i]){
int y=ver[i];
if(y==to[p]||y==f[p])continue;
int root=dfs2(y);
fa[root]=p;
}
}
int top=0;
for(int p=x;p;p=to[p]){
b[top++]=p;
bs[top]=bs[top-1]+s[p]-s[to[p]];
}
return build(0,top);
}
int sum[N],tag[N];
void add(int x){
bool flag=1;int ans=0;
while(x){
sum[x]+=ans;//在经过的每一个点都加上贡献,最后减去右子树的贡献
if(flag){
tag[x]++;if(rc[x])tag[rc[x]]--;//差分打标记
ans+=1+ss[lc[x]];
sum[x]-=ss[rc[x]];
}
flag=(x!=lc[fa[x]]);//判断下次跳的是轻边还是重边,分类讨论
if(x!=lc[fa[x]]&&x!=rc[fa[x]])ans=0;
x=fa[x];
}
}
int ask(int x){
int ret=0;
bool flag=1;int ans=0;
while(x){
if(flag){
ret+=sum[x]-sum[rc[x]];//差分统计答案
ret-=ss[rc[x]]*tag[rc[x]];//减去右子树的标记
ans+=ss[lc[x]]+1;
}
ret+=ans*tag[x];//打上标记
flag=(x!=lc[fa[x]]);//判断下次跳的是轻边还是重边,分类讨论
if(x!=lc[fa[x]]&&x!=rc[fa[x]])ans=0;
x=fa[x];
}
return ret;
}
struct query{int id,pos,x,op;}q[N<<1];
int ans[N];
bool cmp(query&a,query&b){return a.pos<b.pos;}
int main(){
scanf("%d%d",&n,&m);
for(int i=2,x,y;i<=n;i++)scanf("%d",&x),x++,add(x,i),add(i,x);
dfs1(1,0),dfs2(1);
for(int i=1,l,r,x;i<=m;i++){
scanf("%d%d%d",&l,&r,&x);x++;
q[i]={i,l-1,x,-1},q[m+i]={i,r,x,1};//差分答案
}
sort(q+1,q+m*2+1,cmp);
int now=0;
for(int i=1;i<=m*2;i++){
while(now<=q[i].pos)add(++now);
(ans[q[i].id]+=q[i].op*ask(q[i].x))%=mod;
}
for(int i=1;i<=m;i++)printf("%d\n",(ans[i]%mod+mod)%mod);
return 0;
}
应用:
待补的坑:
全局平衡二叉树是解决动态dp的大杀器,可以做做动态dp的两道模板题,P4719和P4751。
再来一道论文中出现的题目:P4115 Qtree4。

浙公网安备 33010602011771号