替罪羊树学习笔记

替罪羊树学习笔记

史!

思想

众所周知,替罪羊树是一种平衡二叉搜索树,其拥有虽然我不理解为什么,但是很牛的复杂度。其思想在于通过一个系数进行定期重构,使得维护所有信息的树是一棵接近平衡树的伪平衡树,那么他依然拥有 \(O(\log n)\) 级别的层高,因此对于跳转查询依旧具有优异的复杂度。

但是,显然对于重构来说,同时需要一定的复杂度,那么我们应该按照什么样的标准评判当前子树是否需要重构呢?这个时候就要用到我们自主决定的系数 \(\alpha\)。对于一棵平衡树来说,我们要求其每一个节点的左右两个子树中的节点数量都尽可能一样,那么此时,我们认为这棵树不失衡。

更多的,如果这棵树上有删除操作,我们采用惰性删除,即只对于当且节点的大小进行维护,即使这个节点已经被删除,我们也认为它仍然存在,只在查询时特判,不对答案产生贡献。那么显然,如果这样名存实亡的节点过多,会使得查询复杂度下降,因此当已经被删除的节点过多时,我们也认为当前子树是失衡的。

重构

根据刚才的思想,我们为重构提出了两个条件:子树大小相当和删除节点较少。那么我们可以维护下面两个信息:子树中的节点大小和子树中有效的节点大小。这样就可以判断了。另外,因为重构本质上只是将节点换了位置,因此编号等信息无需更改,因此,最多插入多少个节点,替罪羊树的空间复杂度就是多少。

当然在没有删除的时候,后面的条件是无用的。

代码

void Update(int id){
    Sz(id)=Sz(L(id))+Sz(R(id))+1;
    Sm(id)=Sm(L(id))+Sm(R(id))+C(id);
    Rs(id)=Rs(L(id))+Rs(R(id))+(C(id)!=0);
    return ;
}//更新信息
void Clear(int id){
    if(id==0)return ;
    Clear(L(id));
    if(C(id)!=0)b[++top]=id;
    Clear(R(id));
    return ;
}//统计需要重构的编号
int Build(int l,int r){
    if(l>r)return 0;
    int mid=(l+r)>>1;
    int id=b[mid];
    for(int i=l;i<=r;i++)t.Insert(id,V(b[i]));
    L(id)=Build(l,mid-1);
    R(id)=Build(mid+1,r);
    Update(id);
    return id;
}//这样子重构出来的一定是平衡到不能再平衡的平衡树
bool IsReb(int id){
    return Sz(id)*A<=(double)max(Sz(L(id)),Sz(R(id)))||Sz(id)*A>=(double)Rs(id);
}//两个条件满足其一即可重构
void Rebuild(int &id){
    top=0;
    Clear(id);
    id=Build(1,top);
    return ;
}//当满足重构条件时才调用这个函数

修改

对于修改,一般分为插入和删除,同时还分为按照权值排序和按照位置排序两种。

权值相关

为了降低相关复杂度,我们选择将所有权值相同的点归为一个点(当然也可以不这么做),让所有节点关于权值是一棵二叉搜索树即可。没有对应的当前节点时,我们直接维护新节点;当前节点的权值等于 \(x\) 时,只用增加个数;当前节点的权值大于 \(x\) 时,朝左子树递归;当前节点的权值小于 \(x\) 时,朝右子树递归。删除操作也基本同理。

代码

void Insert(int &x,int p){
    if(x==0){//新建节点
        x=++tot;
        if(rt==0)rt=1;
        V(x)=p;//维护的对应点的权值
        L(x)=R(x)=0;//维护的左右子树编号
        C(x)=Sz(x)=Sm(x)=Rs(x)=1;//分别是当前节点有多少个数、子树内有多少个节点、子树内有多少个数,子树内有多少个有效节点
        return ;
    }
    if(p==V(x))C(x)++;//只增加节点数的个数
    else if(p<V(x))Insert(L(x),p);//递归左子树
    else Insert(R(x),p);//递归右子树
    Update(x);//更新贡献
    if(IsReb(x))Rebuild(x);//判断是否重构
    return ;
}
void Del(int &x,int p){
    if(x==0)return ;//没有能删除的
    if(p==V(x)){
        if(C(x)!=0)C(x)--;//只减少节点中数的个数
    }else if(p<V(x))Del(L(x),p);//递归左子树
    else Del(R(x),p);//递归右子树
    Update(x);//更新贡献
    if(IsReb(x))Rebuild(x);//判断是否重构
    return ;
}

位置相关

考虑如果每个数在不同的位置上的贡献互不一致,那么就只能按照位置建立二叉搜索树。一般以前面有多少个数为媒介。如果当前节点的前面的数的个数不少于 \(k\) 个,那么新建的节点只能在左子树中;不然新建的节点只能在右子树中。当确定了位置后新建节点即可。对于删除也一样,找到对应位置后只改变节点中数的个数即可。

代码

void Insert(int &id,int p,int k){
    if(id==0){//新建节点
        id=++tot;
        if(rt==0)rt=id;
        V(id)=p;
        C(id)=Sz(id)=Rs(id)=1;//因为一个节点要么有,要么没有,不会有多个数同时在里面,因此不用维护子树内数的个数
        L(id)=R(id)=0;
        return ;
    }
    if(Rs(L(id))>=k)Insert(L(id),p,k);//注意空节点没有贡献
    else Insert(R(id),p,k-Rs(L(id))-C(id));
    Update(id);
    if(IsReb(id))Rebuild(id);
    return ;
}
void Delete(int &id,int k){
    if(Rs(L(id))==k&&C(id)!=0)C(id)--;//注意空节点不会作为被删除的节点
    else if(Rs(L(id))>k)Delete(L(id),k);
    else Delete(R(id),k-Rs(L(id))-C(id));
    Update(id);
    if(IsReb(id))Rebuild(id);
    return ;
}

查询

查询主要有查询第 \(k\) 小/第 \(k\) 位、查询排名(只针对权值)等查询,当然,因为替罪羊树某种意义上来说也是线段树,所以可以按照线段树的思路查询。同样的,查询可以分为关于权值的查询和关于位置的查询。

但是查询没有办法讲解,按照线段树的思路:特判能否直接通过子树维护的信息概括 \(\to\) 分成左子树、右子树递归求解,一般都能求解,可以多多做题,多多积累一些常见的模板。

提醒

首先对于系数的选择,需要先进行粗略的评估,即重构的复杂度和不重构的复杂度那一个占比更大从而选择更合适的系数。一般来说,我们都不希望重构进行太多次,因此我们都会选择能够让重构更少的系数。根据我个人的经验,一般情况下,有删除时 \(\alpha=0.75\) 最优,无删除时 \(\alpha=0.85\) 最优。

接着对于重构,有多种写法进行优化,但是一般针对于无删除。因为在有删除时重构下面的子树可能导致上面的子树从不平衡变得平衡,因此只针对无删除进行优化。更一般的,可以理解为,既然下面的节点不论如何重构都不影响上面的重构结果,那么不妨只重构最上面的位置,这样一定可以优化。但因为重构需要更新左右子树信息,因此同时需要知道重构的节点是隶属上一层的左子树还是右子树。 下面给出代码:

void Insert(int &id,int p,int k){
    if(id==0){
        id=++tot;
        if(rt==0)rt=id;
        V(id)=p;
        C(id)=Sz(id)=Rs(id)=1;
        L(id)=R(id)=0;
        return ;
    }
    if(Rs(L(id))>=k)Insert(L(id),p,k);
    else Insert(R(id),p,k-Rs(L(id))-C(id));
    Update(id);
    if(IsReb(id)){
        S=id;
        F=0;
    }else if(F!=0)F=id;
    return ;
}
void Change(int p,int k){
    Insert(rt,p,k);
    if(s!=0){
        if(f==0)Rebuild(rt);
        else{
            if(L(f)==s)Rebuild(L(f));
            else Rebuild(R(f));
            f=0;
        }
        s=0;
    }
    return ;
}

还有,替罪羊不仅用于平衡树,在一些需要重构树形数据结构的情况下,我们也可以利用这样的思想,也就是当其失衡导致求解层数过大时进行重构。

posted @ 2024-06-08 19:25  DycIsMyName  阅读(10)  评论(0编辑  收藏  举报