全局平衡二叉树

全局平衡二叉树

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的两道模板题,P4719P4751

再来一道论文中出现的题目:P4115 Qtree4

posted @ 2024-10-04 17:55  lichenyu_ac  阅读(122)  评论(0)    收藏  举报