替罪羊树

  1. 参考:

    1. 大佬1的理解和模版:https://zhuanlan.zhihu.com/p/21263304,适合理解替罪羊树的原理,但是没有考虑很多点值都相等的情况,所有的点都是分开的,这点不是很好。

    2. 大佬2的理解和模版:https://www.cnblogs.com/tlx-blog/p/12900730.html,考虑了不同点值相等的情况,都合在一起,每个函数讲解得很好。

  2. 简介:

    1. 名称:替罪羊树(Scapegoat Tree

    2. 本质:优雅的暴力,用少量简单的代码,实现平衡树等能做的事情,经常做到“暴力碾标算”的一种神奇的数据结构~

    3. 一些abstract

      对于一棵二叉搜索树,最重要的事情就是维护他的平衡,以保证对于每次操作(插入,查找,删除)的时间均摊下来都是O(logN)

      而为了维护树的平衡,各种平衡二叉树方法大同小异,本质几乎都是通过旋转的操作来实现(AVL树,红黑树,SplayTreap(可持久化Treap不需要旋转)),只是在判断什么时候应该旋转上有所不同。但是替罪羊树不同,人家不转也能玩得转x。

  3. 核心操作:

    1. 重构:可以重构整棵树,也可以重构子树。操作其实十分暴力,就是把需要重构的树拍平成一条链,显然这条链一定是有序的~。最后把这条链的中点拽起来,作为新的树的根,左边的序列作为左子树,右边的序列作为右子树,递归实现即可。

    2. 插入:

      1. 一些细节:

        一开始和普通的二叉搜索树一样,但是插入操作结束以后,从插入位置开始一层一层往上回溯,每一层都进行如下判断:

        h(v) > log(1/α) × size(tree)(不同人的式子不一样,但是大同小异吧,这里h(v)为左右子树中高度的max,α为一个常数,介于0.51,通常取0.7、0.8、0.75size就是树的节点个数啦~)

        然后一直找到最后不满足这个式子的那一层(就是即将大于右边的那一层,也可以理解为从根节点到这个点的路径中,第一个导致高度不大于右侧的点),也就是h(v)每次加一,早晚会大于右侧,然后重构这一层的子树。

      1. 关于复杂度:

        每次插入的复杂度显然和别的树一样,为O(logn),每次重构树大概是O(n)级别(吃瓜群众:那不炸了吗?你在说什么?!)。但因为不是每次都要重构一整颗树,玄学地均摊下来,复杂度也是O(logn)(有一种平衡树中平衡因子的复杂度的玄学感x),并不用担心复杂度爆炸。

      2. 胡扯阶段:

        有趣的是,因为一个节点导致整个子树被拍扁,然后根节点被提了起来,根节点成了最后的“替罪羊”,这大概就是“替罪羊”名字的由来吧。

    3. 删除:

      1. 删除节点不是真正的删除,而是打标记的惰性删除,打上标记的节点具有如下特征:

        1. 不参与查找操作

        2. 参与其余操作

        3. 当删除的数量超过树节点数的一半(这个也可以变化),重构这棵树。

      2. 关于复杂度:仍然是O(logn),不会证明,知道就行。

    4. 查找第k大的值,以及给一个树查看它的rank:和其余树没什么区别,只是不能计算删除节点。

  4. 代码:搬用https://zhuanlan.zhihu.com/p/21263304的代码,这里只学此数据结构的思想,加了注释(主食x),应该能懂吧。

    #include <vector>
    using namespace std;
    
    namespace Scapegoat_Tree {
    #define MAXN (100000 + 10)
        const double alpha = 0.75;
        struct Node {
        Node * ch[2];
        int key, size, cover; // size为有效节点的数量,cover为节点总数量 
        bool exist;    // 是否存在(即是否被删除) 
        void PushUp(void) {
          size = ch[0]->size + ch[1]->size + (int)exist;
          cover = ch[0]->cover + ch[1]->cover + 1;
            }
        bool isBad(void) { // 判断是否需要重构 
          return ((ch[0]->cover > cover * alpha + 5) || (ch[1]->cover > cover * alpha + 5));
        }
        };
        struct STree {
        protected:
            Node mem_poor[MAXN]; //内存池,直接分配好避免动态分配内存占用时间 
            Node *tail, *root, *null; // 用null表示NULL的指针更方便,tail为内存分配指针,root为根 
            Node *bc[MAXN]; int bc_top; // 储存被删除的节点的内存地址,分配时可以再利用这些地址 
    
            Node * NewNode(int key) {
                Node * p = bc_top ? bc[--bc_top] : tail++;
                p->ch[0] = p->ch[1] = null;
                p->size = p->cover = 1; p->exist = true;
                p->key = key;
                return p;
            }
            void Travel(Node * p, vector<Node *>&v) {  // 中序遍历 得到所有还活着的点的序列
                if (p == null) return;
                Travel(p->ch[0], v);
                if (p->exist) v.push_back(p); // 构建序列 
                else bc[bc_top++] = p; // 回收 
                Travel(p->ch[1], v);
            }
            Node * Divide(vector<Node *>&v, int l, int r) {  // 根据序列得到每个节点的儿子信息
                if (l >= r) return null;
                int mid = (l + r) >> 1;
                Node * p = v[mid];
                p->ch[0] = Divide(v, l, mid);
                p->ch[1] = Divide(v, mid + 1, r);
                p->PushUp(); // 自底向上维护,先维护子树 
                return p;
            }
            void Rebuild(Node * &p) {  // 重构包括重新拿到存活的节点,以及平衡地得到每个节点的儿子信息
                static vector<Node *>v; v.clear();
                Travel(p, v); p = Divide(v, 0, v.size());
            }
            Node ** Insert(Node *&p, int val) {  // 插入操作
                if (p == null) {
                    p = NewNode(val);
                    return &null;
                }
                else {
                    p->size++; p->cover++;
                    
                    // 返回值储存需要重构的位置,若子树也需要重构,本节点开始也需要重构,以本节点为根重构 
                    Node ** res = Insert(p->ch[val >= p->key], val);
                    if (p->isBad()) res = &p;  // 如果这层也沦陷了就更新,否则就是子树的答案一直返回
                    return res;
                }
            }
            void Erase(Node *p, int id) {  // 删掉第id大的
                p->size--;
                int offset = p->ch[0]->size + p->exist;  // 根节点是第几大的
                if (p->exist && id == offset) {  // 删就对了
                    p->exist = false;
                    return;
                }
                else {
                    if (id <= offset) Erase(p->ch[0], id);  // 往左闪
                    else Erase(p->ch[1], id - offset);  // 往右删
                }
            }
        public:
            void Init(void) {
                tail = mem_poor;
                null = tail++;
                null->ch[0] = null->ch[1] = null;
                null->cover = null->size = null->key = 0;
                root = null; bc_top = 0;
            }
            STree(void) { Init(); }
    
            void Insert(int val) {
                Node ** p = Insert(root, val);
                if (*p != null) Rebuild(*p);  // 拿到rebuild的位置,开始重构
            }
            int Rank(int val) {  // 拿到val值的排名,如果有重复的取拍平序列的第一次出现的位置
                Node * now = root;
                int ans = 1;
                while (now != null) { // 非递归求排名 
                    if (now->key >= val) now = now->ch[0];
                    else {
                        ans += now->ch[0]->size + now->exist;
                        now = now->ch[1];
                    }
                }
                return ans;
            }
            int Kth(int k) {
                Node * now = root;
                while (now != null) { // 非递归求第K大 
                    if (now->ch[0]->size + 1 == k && now->exist) return now->key;  // 只有这样才是自己
                    else if (now->ch[0]->size >= k) now = now->ch[0];  // 往左找
                    else k -= now->ch[0]->size + now->exist, now = now->ch[1];  // 往右找,别忘了更新k
                }
            }
            void Erase(int k) {  // 删掉值为k的点
                Erase(root, Rank(k));  // 但是注意 如果值为k的点不在这个树里,这样是不对的。
                if (root->size < alpha * root->cover) Rebuild(root);  // 重构条件
            }
            void Erase_kth(int k) {
                Erase(root, k);
                if (root->size < alpha * root->cover) Rebuild(root);  // 重构条件
            }
        };
    #undef MAXN
    }
    
    INLINE void read(int &x) {
            static char c; c = NC(); int b = 1;
            for (x = 0; !(c >= '0' && c <= '9'); c = NC()) if(c == '-') b = -b;
            for (; c >= '0' && c <= '9'; x = x * 10 + c - '0', c = NC()); x *= b;
    }
    using namespace Scapegoat_Tree;
     
    STree _t;
    int n, k, m;
    int main(void) {
            //freopen("in.txt", "r", stdin);
            //freopen("out.txt", "w", stdout);
            read(n);
            while (n--) {
                    read(k), read(m);
                    switch (k) {
                    case 1: _t.Insert(m); break;
                    case 2: _t.Erase(m); break;
                    case 3: printf("%d\n", _t.Rank(m)); break;
                    case 4: printf("%d\n", _t.Kth(m)); break;
                    case 5: printf("%d\n", _t.Kth(_t.Rank(m) - 1)); break;
                    case 6: printf("%d\n", _t.Kth(_t.Rank(m + 1))); break;
                    }
                    /* DEBUG INFO
                    vector<Node *> xx;
                    _t.Travel(_t.root, xx);
                    cout << "::";
                    for(int i = 0; i < xx.size(); i++) cout << xx[i]->key << ' '; cout << endl;
                    */
            }
            return 0;
    }

    val);
     if (p->isBad()) res &p;  // 如果这层也沦陷了就更新,否则就是子树的答案一直返回 
    return res; 


    void Erase(Node *p, int id) {  // 删掉第id大的 
    p->size--; 
    int offset p->ch[0]->size p->exist;  // 根节点是第几大的 
    if (p->exist && id == offset) {  // 删就对了 
    p->exist false; 
    return; 

    else { 
    if (id <= offset) Erase(p->ch[0], id);  // 往左闪 
    else Erase(p->ch[1], id offset);  // 往右删 


    public: 
    void Init(void) { 
    tail mem_poor; 
    null tail++; 
    null->ch[0] null->ch[1] null; 
    null->cover null->size null->key 0; 
    root null; bc_top 0; 

    STree(void) { Init(); } 
    ​ 
    void Insert(int val) { 
    Node ** Insert(root, val); 
    if (*!= null) Rebuild(*p);  // 拿到rebuild的位置,开始重构 

    int Rank(int val) {  // 拿到val值的排名,如果有重复的取拍平序列的第一次出现的位置 
    Node now root; 
    int ans 1; 
    while (now != null) { // 非递归求排名  
    if (now->key >= val) now now->ch[0]; 
    else { 
    ans += now->ch[0]->size now->exist; 
    now now->ch[1]; 


    return ans; 

    int Kth(int k) { 
    Node now root; 
    while (now != null) { // 非递归求第K大  
    if (now->ch[0]->size == && now->exist) return now->key;  // 只有这样才是自己 
    else if (now->ch[0]->size >= k) now now->ch[0];  // 往左找 
    else -= now->ch[0]->size now->exist, now now->ch[1];  // 往右找,别忘了更新k 


    void Erase(int k) {  // 删掉值为k的点 
    Erase(root, Rank(k));  // 但是注意 如果值为k的点不在这个树里,这样是不对的。 
    if (root->size alpha root->cover) Rebuild(root);  // 重构条件 

    void Erase_kth(int k) { 
    Erase(root, k); 
    if (root->size alpha root->cover) Rebuild(root);  // 重构条件 

    }; 
    #undef MAXN 

    ​ 
    INLINE void read(int &x) { 
            static char c; NC(); int 1; 
            for (0; !(>= '0' && <= '9'); NC()) if(== '-') -b; 
            for (; >= '0' && <= '9'; 10 '0', NC()); *= b; 

    using namespace Scapegoat_Tree; 
      
    STree _t; 
    int n, k, m; 
    int main(void) { 
            //freopen("in.txt", "r", stdin); 
            //freopen("out.txt", "w", stdout); 
            read(n); 
            while (n--) { 
                    read(k), read(m); 
                    switch (k) { 
                    case 1: _t.Insert(m); break; 
                    case 2: _t.Erase(m); break; 
                    case 3: printf("%d\n", _t.Rank(m)); break; 
                    case 4: printf("%d\n", _t.Kth(m)); break; 
                    case 5: printf("%d\n", _t.Kth(_t.Rank(m) 1)); break; 
                    case 6: printf("%d\n", _t.Kth(_t.Rank(1))); break; 
                   } 
                    /* DEBUG INFO 
                    vector<Node *> xx; 
                    _t.Travel(_t.root, xx); 
                    cout << "::"; 
                    for(int i = 0; i < xx.size(); i++) cout << xx[i]->key << ' '; cout << endl; 
                    */ 
           } 
            return 0; 
    }

    当然,这个代码看起来对于一样的数字,会存到不同的节点,即每个节点只存一个数字,多个数字一样的情况,就会很浪费内存,大概只需要在点那里加一个重复值的数量,然后值为0的时候,existfalse吧,然后找rank之类的操作把now->exist变成now->count大概就行了吧(口胡的,错了轻点喷)。

    然后第二位大佬的代码:

    #include"iostream"
    #include"cstdio"
    #include"cmath"
    #include"cstring"
    using namespace std;
    
    #define read(x) scanf("%d",&x)
    #define MAXN 100005
    
    const double alpha=0.75;  // 这个值随心就好了
    int n;
    int t,x;
    struct node
    {
        int ls,rs;
        int size,sh;
        int val;
        int wn;
        node()
        {
            ls=rs=0;
            size=sh=0;
            val=0;
            wn=0;
        }
    }a[MAXN];
    int root=0;
    int rt[MAXN],crt=0;
    int cnt=0;
    
    bool judge(int k){return (a[k].wn&&(alpha*(double)a[k].size<(double)max(a[a[k].ls].size,a[a[k].rs].size)||(double)a[k].sh<alpha*(double)a[k].size));}
    
    void update(int k)
    {
        a[k].size=a[a[k].ls].size+a[a[k].rs].size+a[k].wn;
        a[k].sh=a[a[k].ls].sh+a[a[k].rs].sh+a[k].wn;
        return;
    }
    
    void unfold(int k)
    {
        if(!k) return;
        unfold(a[k].ls);
        if(a[k].wn) rt[++crt]=k;
        unfold(a[k].rs);
        return;
    }
    
    int rebuild(int l,int r)
    {
        if(l==r) return 0;
        int mid=(l+r)>>1;
        a[rt[mid]].ls=rebuild(l,mid);
        a[rt[mid]].rs=rebuild(mid+1,r);
        update(rt[mid]);
        return rt[mid];
    }
    
    void bal(int& k){crt=0,unfold(k),k=rebuild(1,crt+1);}
    
    void insert(int& k,int x)
    {
        if(!k)
        {
            k=++cnt;
            if(!root) root=1;
            a[k].val=x,a[k].ls=a[k].rs=0;
            a[k].wn=a[k].size=a[k].sh=1;
        }
        else
        {
            if(a[k].val==x) a[k].wn++;
            else if(x<a[k].val) insert(a[k].ls,x);
            else insert(a[k].rs,x);
            update(k);
            if(judge(k)) bal(k);
        }
    }
    
    void del(int& k,int x)
    {
        a[k].sh--;
        if(a[k].val==x) a[k].wn--;
        else
        {
            if(a[k].val>x) del(a[k].ls,x);
            else del(a[k].rs,x);
        }
        update(k);
        if(judge(k)) bal(k);
    }
    
    int rkup(int k,int x)
    {
        if(!k) return 1;
        if(a[k].wn&&x==a[k].val) return 1+a[k].wn+a[a[k].ls].sh;
        else if(x<a[k].val) return rkup(a[k].ls,x);
        else return a[a[k].ls].sh+a[k].wn+rkup(a[k].rs,x); 
    }
    
    int rkdown(int k,int x)
    {
        if(!k) return 0;
        if(a[k].wn&&a[k].val==x) return a[a[k].ls].sh;
        else if(x<a[k].val) return rkdown(a[k].ls,x);
        else return a[a[k].ls].sh+a[k].wn+rkdown(a[k].rs,x);    
    }
    
    int at(int k,int x)
    {
        if(a[k].ls==a[k].rs) return a[k].val;
        if(x<=a[a[k].ls].sh) return at(a[k].ls,x);
        else if(x>a[a[k].ls].sh&&a[a[k].ls].sh+a[k].wn>=x) return a[k].val;
        else return at(a[k].rs,x-a[a[k].ls].sh-a[k].wn); 
    }
    
    int main()
    {
        read(n);
        while(n--)
        {
            read(t),read(x);
            if(t==1) insert(root,x);
            else if(t==2) del(root,x);
            else if(t==3) printf("%d\n",rkdown(root,x)+1);
            else if(t==4) printf("%d\n",at(root,x));
            else if(t==5) printf("%d\n",at(root,rkdown(root,x)));
            else printf("%d\n",at(root,rkup(root,x)));
        }
        return 0;
    }
    else del(a[k].rs,x); 

    update(k); 
    if(judge(k)) bal(k); 

    ​ 
    int rkup(int k,int x) 

    if(!k) return 1; 
    if(a[k].wn&&x==a[k].val) return 1+a[k].wn+a[a[k].ls].sh; 
    else if(x<a[k].val) return rkup(a[k].ls,x); 
    else return a[a[k].ls].sh+a[k].wn+rkup(a[k].rs,x);  

    ​ 
    int rkdown(int k,int x) 

    if(!k) return 0; 
    if(a[k].wn&&a[k].val==x) return a[a[k].ls].sh; 
    else if(x<a[k].val) return rkdown(a[k].ls,x); 
    else return a[a[k].ls].sh+a[k].wn+rkdown(a[k].rs,x); 

    ​ 
    int at(int k,int x) 

    if(a[k].ls==a[k].rs) return a[k].val; 
    if(x<=a[a[k].ls].sh) return at(a[k].ls,x); 
    else if(x>a[a[k].ls].sh&&a[a[k].ls].sh+a[k].wn>=x) return a[k].val; 
    else return at(a[k].rs,x-a[a[k].ls].sh-a[k].wn);  

    ​ 
    int main() 

    read(n); 
    while(n--) 

    read(t),read(x); 
    if(t==1) insert(root,x); 
    else if(t==2) del(root,x); 
    else if(t==3) printf("%d\n",rkdown(root,x)+1); 
    else if(t==4) printf("%d\n",at(root,x)); 
    else if(t==5) printf("%d\n",at(root,rkdown(root,x))); 
    else printf("%d\n",at(root,rkup(root,x))); 

    return 0; 
    }
  5. 杂谈:

    1. 关于复杂度:替罪羊树和splay为均摊数据结构,但是可持久化复杂度会退化(卡T方法:不断回到树的最坏状态进行操作,即不断地Ctrl-Z, Ctrl-Y,就不用谈均摊了,每次都是最劣复杂度),而一般的平衡树都可以可持久化,尤其fhq\ treap更容易写。替罪羊树复杂度是O(nlogn),第一个代表操作数,第二个代表节点数,因为节点数大概和n也是成正比,就这么写了。

    2. 那看起来不能支持可持久,学他有啥意义(by 百度百科):

      ①:思维难度小;

      ②:代码量少,思路清晰,实现简单,容易调试;

      ③:速度不慢;(常数优势巨大,遇到卡T的题,可能要尝试一下了x)

      ④:不是旋转机制;

    3. 待补充

posted on 2022-05-03 10:22  小染子  阅读(47)  评论(0)    收藏  举报