题解 P3369 【【模板】普通平衡树】

· · 题解

刚刚恶补了替罪羊树,由于一个指针出锅调了贼久,必须得写一篇题解巩固一下理解。

参考了ikka大佬的博客,我的替罪羊树就是在那里学会的。

替罪羊树是一种优越的平衡树,它不像Splay和Treap有着绚丽的旋转操作,而是朴实地暴力重构。

具体来说,为了保持树结构的平衡,替罪羊树在每一次插入和删除的时候都会判断树是否平衡,根据结果选择是否要重构子树。

明白了替罪羊树的思想之后实现其实不难(毕竟思路朴实)。

平衡判断

替罪羊树最核心的部分,也就是它判断树是否平衡的过程,是通过平衡因子来实现的。

这个名字听起来很霸气很高深,但其实就是一个0.5到1之间的浮点数罢了。

const double alpha = 0.75;

当一颗树的左子树(或右子树)存在的节点数大于这棵树存在的节点数*平衡因子的时候,这颗树就是不平衡的。

这个过程我们会在后面用到。

节点结构体

struct Node{
    int val,size,cover;
    bool exist;
    Node* ch[2];
    void pushup(){this->size=ch[0]->size+ch[1]->size+(int)exist,this->cover=ch[0]->cover+ch[1]->cover+1;}
    int isbad(){return (ch[0]->cover>this->cover*alpha+5)||(ch[1]->cover>this->cover*alpha+5);}
};

val是节点的值,size是节点子树的大小,cover是节点存在的的节点个数。

exist是节点是否存在

ch是左儿子与右儿子。

pushup操作很好理解,就是size和cover都更新一下。

isbad操作判断当前子树是否是不平衡的,原理就是之前讲的那个过程。(之所以加上5是怕不稳)

你可能会注意到,本文多次强调了“存在”这个概念,没错,替罪羊树不会真正删去节点,而是将它们变为“不存在”。

新节点

你可能会认为新建节点就是new node,然鹅这样太慢了。

为了提速,我们必须手动模拟内存池(其实我听到这个词是懵逼的)。

模拟内存池的流程就是,把所有不要的节点丢进内存回收池,然后需要的时候再提取出来。

这样删除掉的节点就不会浪费,而能得到有效利用。

Node mempool[N];
Node *tail,*null,*root;
Node *bc[N];
int bc_top;
Node* newNode(int val){
    Node* p=bc_top?bc[--bc_top]:tail++;
    p->ch[0]=p->ch[1]=null;
    p->size=p->cover=p->exist=1;
    p->val=val;
    return p;
}

mempool就是我们模拟的内存池。tail指向内存池内的元素。

root不用管它。

null是我们模拟的空节点其实没什么用

bc是内存回收值,bc_top是回收池顶。

创建一个新节点的流程。。。其实我觉得不说你们也看得懂,那就不说了。

重构子树

这是最核心的部分,其根本思路是讲树拍成一个有序数列,然后再重新建树。

排成有序数列做一个中序遍历就可以了,只有存在的节点才加入数列。

建树也很简单,就每次数组折半,分治建即可。

void Travel(Node* p,vector<Node*>& x){
    if(p==null)return;
    Travel(p->ch[0],x);
    if(p->exist)x.push_back(p);
    else bc[bc_top++]=p;
    Travel(p->ch[1],x);
}
Node* Divide(vector<Node*>& x,int l,int r){
    if(l>=r)return null;
    int mid=(l+r)>>1;
    Node* p=x[mid];
    p->ch[0]=Divide(x,l,mid);
    p->ch[1]=Divide(x,mid+1,r);
    p->pushup();
    return p;
}
void Rebuild(Node*& p){
    vector<Node*> x;
    x.clear();
    Travel(p,x);
    p=Divide(x,0,x.size());
}

数组你用啥都行,由于懒这里用vector。

实现没有什么好讲的。

插入节点

我个人认为,插入节点是替罪羊树最毒瘤的地方(指针不友好)。

插入节点在插入的同时,会返回指向指向不平衡的节点的指针的指针。

上面那句话就是我所谓的毒瘤(其实也不是很毒瘤),第一次可能会被绕地晕头转向。

之所以要返回指向指针的指针,是因为我们所有操作都需要指针。

Node** Insert(Node*& p,int val){
    if(p==null){
        p=newNode(val);
        return &null;
    }
    p->size++,p->cover++;
    Node** res=Insert(p->ch[val>=p->val],val);
    if(p->isbad())res=&p;
    return res;
}

如果当前节点是空节点,那就新建一个节点,然后返回空节点(新的节点不会导致树不平衡)。

否则根据插入节点的值递归左右儿子。

然后假如当前节点子树不平衡,返回当前节点。

删除操作

删除操作很水,没什么技术含量。

void Erase(Node*& p,int k){
    p->size--;
    int offset=p->ch[0]->size+p->exist;
    if(p->exist&&offset==k){
        p->exist=false;
    }else{
        if(k<=offset)Erase(p->ch[0],k);
        else Erase(p->ch[1],k-offset);
    }
}

初始化

顺带一提,从本函数开始是非工具函数(之前的都是protected,并非外部直接调用的函数)。

void init(){
    tail=mempool;
    null=tail++;
    null->ch[0]=null->ch[1]=null;
    null->cover=null->size=null->exist=0;
    root=null;
    bc_top=0;
}

初始化就搞一个空节点,然后没了。

其他操作

这些操作都是建立在工具函数的基础上,YY出来的。

要注意插入和删除都得检查树是否平衡。

然后查询rank和第K大都是循环实现,思路很朴实。

void insert(int val){
    Node** res=Insert(root,val);
    if(*res!=null)Rebuild(*res);
}
int rank(int val){
    Node* now=root;
    int ans=1;
    while(now!=null){
        if(now->val>=val)now=now->ch[0];
        else{
            ans+=now->ch[0]->size+now->exist;
            now=now->ch[1];
        }
    }
    return ans;
}
int kth(int val){
    Node* now=root;
    while(now!=null){
        if(now->ch[0]->size+1==val&&now->exist)return now->val;
        if(now->ch[0]->size>=val)now=now->ch[0];
        else val-=now->ch[0]->size+now->exist,now=now->ch[1];
    }
}
void erase(int k){
    Erase(root,rank(k));
    if(root->size<root->cover*alpha)Rebuild(root);
}

代码

AC代码放心使用

#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
namespace Scapegoat{
    const int N=100100;
    const double alpha = 0.75;
    struct Node{
        int val,size,cover;
        bool exist;
        Node* ch[2];
        void pushup(){this->size=ch[0]->size+ch[1]->size+(int)exist,this->cover=ch[0]->cover+ch[1]->cover+1;}
        int isbad(){return (ch[0]->cover>this->cover*alpha+5)||(ch[1]->cover>this->cover*alpha+5);}
    };
    struct Tree{
        protected:
            Node mempool[N];
            Node *tail,*null,*root;
            Node *bc[N];
            int bc_top;
            Node* newNode(int val){
                Node* p=bc_top?bc[--bc_top]:tail++;
                p->ch[0]=p->ch[1]=null;
                p->size=p->cover=p->exist=1;
                p->val=val;
                return p;
            }
            void Travel(Node* p,vector<Node*>& x){
                if(p==null)return;
                Travel(p->ch[0],x);
                if(p->exist)x.push_back(p);
                else bc[bc_top++]=p;
                Travel(p->ch[1],x);
            }
            Node* Divide(vector<Node*>& x,int l,int r){
                if(l>=r)return null;
                int mid=(l+r)>>1;
                Node* p=x[mid];
                p->ch[0]=Divide(x,l,mid);
                p->ch[1]=Divide(x,mid+1,r);
                p->pushup();
                return p;
            }
            void Rebuild(Node*& p){
                static vector<Node*> x;
                x.clear();
                Travel(p,x);
                p=Divide(x,0,x.size());
            }
            Node** Insert(Node*& p,int val){
                if(p==null){
                    p=newNode(val);
                    return &null;
                }
                p->size++,p->cover++;
                Node** res=Insert(p->ch[val>=p->val],val);
                if(p->isbad())res=&p;
                return res;
            }
            void Erase(Node*& p,int k){
                p->size--;
                int offset=p->ch[0]->size+p->exist;
                if(p->exist&&offset==k){
                    p->exist=false;
                }else{
                    if(k<=offset)Erase(p->ch[0],k);
                    else Erase(p->ch[1],k-offset);
                }
            }
        public:
            void init(){
                tail=mempool;
                null=tail++;
                null->ch[0]=null->ch[1]=null;
                null->cover=null->size=null->exist=0;
                root=null;
                bc_top=0;
            }
            Tree(){
                init();
            }
            void insert(int val){
                Node** res=Insert(root,val);
                if(*res!=null)Rebuild(*res);
            }
            int rank(int val){
                Node* now=root;
                int ans=1;
                while(now!=null){
                    if(now->val>=val)now=now->ch[0];
                    else{
                        ans+=now->ch[0]->size+now->exist;
                        now=now->ch[1];
                    }
                }
                return ans;
            }
            int kth(int val){
                Node* now=root;
                while(now!=null){
                    if(now->ch[0]->size+1==val&&now->exist)return now->val;
                    if(now->ch[0]->size>=val)now=now->ch[0];
                    else val-=now->ch[0]->size+now->exist,now=now->ch[1];
                }
            }
            void erase(int k){
                Erase(root,rank(k));
                if(root->size<root->cover*alpha)Rebuild(root);
            }
    };
}
using namespace Scapegoat;
Tree tree;
int main(){
    int n,op,x;
    scanf("%d",&n);
    while(n--){
        scanf("%d%d",&op,&x);
        if(op==1)tree.insert(x);
        if(op==2)tree.erase(x);
        if(op==3)printf("%d\n",tree.rank(x));
        if(op==4)printf("%d\n",tree.kth(x));
        if(op==5)printf("%d\n",tree.kth(tree.rank(x)-1));
        if(op==6)printf("%d\n",tree.kth(tree.rank(x+1)));
    }
    return 0;
}

然后就没了。

后记

有一天我一个ZZ同学跟我说替罪羊树乃至平衡树提高组不考

o_0??WTF?????????