zoukankan      html  css  js  c++  java
  • 替罪羊树摘要

    原理

    替罪羊树不依靠旋转,而是依靠重构不平衡的子树使整棵树达到平衡状态。

    变量介绍

     1 int n;
     2 struct Node {
     3     int val,num;                                                                    //节点的值,数量 
     4     int siz,hid;                                                                    //以该节点为根的子树中未被删的节点数和已被删的节点数 
     5     int son[2],fa;                                                                    //左(0)右(1)儿子,爸爸 
     6     char id;                                                                        //是否被删(0否1是) 
     7     void res() {                                                                    //清空,用于重构 
     8         hid=fa=val=num=siz=son[0]=son[1]=id=0;
     9     }
    10 } tree[maxn];
    11 int ins,mem[maxn],inm,root;                                                            //新节点内存回收池 
    12 int reb[maxn],ren[maxn],inr;                                                        //临时数组,重构用 
    var

    各种操作

    暴力重构

    当一颗子树不平衡时,便将其重构,这是替罪羊树的核心思想。判断一颗子树是否平衡的方法有很多,我采用的方法是,若一颗子树的某个儿子子树的大小大于这颗子树的大小的alpha倍,或这颗子树被删除的节点数超过这颗子树总结点数的alpha倍,则认为这颗子树是不平衡的,需要重构。我们也可以知道,当alpha>=1或alpha<=0.5的时候无意义。alpha越大,重构次数越少,但这颗树越不平衡;alpha越小,树越平衡,但重构次数越多。因此,我们折中取alpha=0.75。(由于不想用浮点数,这里改成了整数乘法运算)

    1 char need_to_reset(int now) {                                                        //以当前节点为根的子树是否需要重构 
    2     return tree[now].siz*3<tree[tree[now].son[0]].siz*4
    3     ||tree[now].siz*3<tree[tree[now].son[1]].siz*4||tree[now].siz<tree[now].hid*3;    //若某颗子树大小超过整棵子树的3/4或
    4                                                                                     //被删除的节点超过总结点数的3/4则需重构 
    5 }
    need_to_reset

    重构有两种方法,我采用的是时空复杂度O(n)的中序遍历法(ps:所谓拍扁重构法是用链表的时间O(n)空间O(logn)的方法,本蒟蒻不会)。将需要的子树中序遍历,得到一个序列,再每次二分,取最中间的那个数建根,左右递归建树。

     1 void redfs(int now) {
     2     if(tree[now].son[0]) redfs(tree[now].son[0]);
     3     if(!tree[now].id)reb[++inr]=tree[now].val,ren[inr]=tree[now].num;                //若没被删除则加入临时数组,否则不加入 
     4     if(tree[now].son[1]) redfs(tree[now].son[1]);
     5     mem[++inm]=now;
     6     tree[now].res();                                                                //清空 
     7 }
     8 void rebuild(int l,int r,int pla,int f) {
     9     if(l>r)return;
    10     if(l==r) {
    11         tree[pla].val=reb[l];
    12         tree[pla].siz=tree[pla].num=ren[l];
    13         tree[pla].fa=f;
    14         return;
    15     }
    16     int mid=l+r>>1;
    17     tree[pla].val=reb[mid];
    18     int s1,s2;
    19     tree[pla].siz=tree[pla].num=ren[mid];
    20     tree[pla].fa=f;
    21     if(l<mid) {                                                                        //建左子树 
    22         s1=tree[pla].son[0]=mem[inm--];
    23         rebuild(l,mid-1,s1,pla);
    24     }
    25     if(mid<r) {                                                                        //建右子树 
    26         s2=tree[pla].son[1]=mem[inm--];
    27         rebuild(mid+1,r,s2,pla);
    28     }
    29     update(pla);
    30 }
    31 void reset(int rot) {
    32     int f=tree[rot].fa;
    33     inr=0;                                                                            //这个清零应该不会忘 
    34     redfs(rot);
    35     rebuild(1,inr,((root==rot)?root=mem[inm--]:mem[inm--]),f);                        //注意,若需重构整棵树则要更新root 
    36     for(;tree[rot].fa;rot=tree[rot].fa)update(tree[rot].fa);
    37 }
    reset

    插入

    和普通的平衡树一样插入,注意要用递归,并且传参的时候传一个引用,用来存回溯时最浅的需要被重构的子树根编号。

     1 void insert(int now,int x,int &ntr) {                                                //ntr为引用,因为需要在回溯时找到最浅的替罪羊节点
     2                                                                                     //注意一定要用递归式的,用循环式的就会像我一样WA上天
     3                                                                                     //删除也是一样 
     4     if(!now) {                                                                        //走到这个if里面说明整棵树为空 
     5         int u=(inm?mem[inm--]:++ins);
     6         tree[u].siz=tree[u].num=1;
     7         tree[u].val=x;
     8         root=u;
     9         return;
    10     }
    11     if(tree[now].val==x) {                                                            //当前节点的值等于需插入的值 
    12         if(tree[now].id) tree[now].id=0,tree[now].hid--;                            //如果被删除则恢复 
    13         tree[now].siz++,tree[now].num++;
    14         if(need_to_reset(now)) ntr=now;                                                //随手一判 
    15         return;
    16     }
    17     if(tree[now].son[tree[now].val<x]) insert(tree[now].son[tree[now].val<x],x,ntr);//往正确的儿子走 
    18     else {                                                                            //走到底了 
    19         int u=(inm?mem[inm--]:++ins);
    20         tree[u].siz=tree[u].num=1;
    21         tree[u].fa=now;
    22         tree[u].val=x;
    23         tree[now].son[tree[now].val<x]=u;
    24     }
    25     update(now);
    26     if(need_to_reset(now)) ntr=now;
    27 }
    insert

    删除

    替罪羊树的删除并不是真正意义上的删除,而是在这个数被删除消失时打上一个删除标记,当重构时再真正删除,或再次添加这个数来取消这个标记。(ps:这个思想在许多数据结构中都非常有用)

     1 void delet(int now,int x,int &ntr) {
     2     if(tree[now].val==x) {
     3         if(tree[now].num) tree[now].num--,tree[now].siz--,tree[now].hid++;
     4         if(!tree[now].num) tree[now].id=1;
     5         if(need_to_reset(now)&&(tree[now].son[0]||tree[now].son[1])) ntr=now;        //如果这个点是叶子节点就不判,不要问我为什么 
     6         return;
     7     }
     8     if(tree[now].son[tree[now].val<x]) delet(tree[now].son[tree[now].val<x],x,ntr);
     9     update(now);
    10     if(need_to_reset(now)) ntr=now;
    11 }
    delet

    查询x排名

    和其他平衡树一样查找就行了,无所谓递归或循环。

    1 int findran(int x) {
    2     int u=root,ans=1;
    3     for(; u&&tree[u].val!=x; u=tree[u].son[x>tree[u].val])                            //不需要判重构,所以可以用循环 
    4         ans+=(x>tree[u].val)*(tree[tree[u].son[x<tree[u].val]].siz+tree[u].num);
    5     if(u) ans+=tree[tree[u].son[0]].siz;
    6     return ans;
    7 }
    finran

    查询排名为x的数

    和其他平衡树一样查找就行了。

    1 int findnum(int x) {
    2     int u=root;
    3     for(char t; tree[u].son[tree[tree[u].son[0]].siz+tree[u].num<x?1:0]&&
    4         (tree[tree[u].son[0]].siz>=x||tree[tree[u].son[0]].siz+tree[u].num<x);
    5         t=tree[tree[u].son[0]].siz+tree[u].num<x,
    6         x-=t*(tree[tree[u].son[0]].siz+tree[u].num),u=tree[u].son[t]);
    7     return tree[u].val;
    8 }
    finnum

    查询x前驱

    先找到这个数的排名y,再找到排名为y-1的数就是x的前驱。

    1 int las(int x) {
    2     int rnk=findran(x);                                                                //先找到这个点的排名 
    3     return findnum(rnk-1);                                                            //再找到比它小的最大的数 
    4 }
    las

    查询x后继

    先找到这个数的最大排名(即这个数的排名+这个数的个数-1)y,再找到排名为y+1的数就是x的后继。

    1 int nex(int x) {
    2     int u=root,ans=0;                                                                //不要问我为什么这个写得这么丑 
    3     for(; u&&tree[u].val!=x; u=tree[u].son[x>tree[u].val])                            //找到这个数的最大排名 
    4         ans+=(x>tree[u].val)*(tree[tree[u].son[x<tree[u].val]].siz+tree[u].num);
    5     if(u) ans+=tree[tree[u].son[0]].siz+tree[u].num;
    6     return findnum(ans+1);                                                            //查询比它大的最小的数 
    7 }
    nex

    时空复杂度

    时间复杂度

    重构:单次重构的复杂度为O(n),但通过势能分析可以得出重构的均摊复杂度为O(logn)。推导过程来自VFleaking大佬的论文(Orzzzzzz)。

    插入、删除、查询:由于保证树高均摊logn,因此复杂度为均摊O(logn)

    常数还可以,似乎仅比红黑树慢,但单一的alpha容易被卡。因此可以开始就随机若干个alpha,取出合格的取平均值

    空间复杂度

    O(n)

    例题

    洛谷P3369【模板】普通平衡树

     1 #include<bits/stdc++.h>
     2 using namespace std;
     3 #define INF 0x7fffffff
     4 #define ME 0x7f
     5 #define FO(s) freopen(s".in","r",stdin);freopen(s".out","w",stdout)
     6 #define fui(i,a,b,c) for(int i=(a);i<=(b);i+=(c))
     7 #define fdi(i,a,b,c) for(int i=(a);i>=(b);i-=(c))
     8 #define fel(i,a) for(register int i=h[a];i;i=ne[i])
     9 #define ll long long
    10 #define MEM(a,b) memset(a,b,sizeof(a))
    11 #define maxn (100000+10)
    12 int n;
    13 struct Node{int val,num;int siz,hid;int son[2],fa;char id;void res(){hid=fa=val=num=siz=son[0]=son[1]=id=0;}}tree[maxn];
    14 int ins,mem[maxn],inm,root;
    15 int reb[maxn],ren[maxn],inr;
    16 template<class T>
    17 inline T read(T &n){
    18     n=0;int t=1;double x=10;char ch;
    19     for(ch=getchar();!isdigit(ch)&&ch!='-';ch=getchar());(ch=='-')?t=-1:n=ch-'0';
    20     for(ch=getchar();isdigit(ch);ch=getchar()) n=n*10+ch-'0';
    21     if(ch=='.') for(ch=getchar();isdigit(ch);ch=getchar()) n+=(ch-'0')/x,x*=10;
    22     return (n*=t);
    23 }void update(int x){
    24     tree[x].siz=tree[tree[x].son[0]].siz+tree[tree[x].son[1]].siz+tree[x].num;
    25     tree[x].hid=tree[tree[x].son[0]].hid+tree[tree[x].son[1]].hid+tree[x].id;
    26 }void redfs(int now){if(tree[now].son[0]) redfs(tree[now].son[0]);
    27     if(!tree[now].id)reb[++inr]=tree[now].val,ren[inr]=tree[now].num;
    28     if(tree[now].son[1]) redfs(tree[now].son[1]);mem[++inm]=now;tree[now].res();
    29 }void rebuild(int l,int r,int pla,int f){if(l>r)return;
    30     if(l==r){tree[pla].val=reb[l];tree[pla].siz=tree[pla].num=ren[l];tree[pla].fa=f;return;}
    31     int mid=l+r>>1;tree[pla].val=reb[mid];int s1,s2;tree[pla].siz=tree[pla].num=ren[mid];tree[pla].fa=f;
    32     if(l<mid){s1=tree[pla].son[0]=mem[inm--];rebuild(l,mid-1,s1,pla);}
    33     if(mid<r){s2=tree[pla].son[1]=mem[inm--];rebuild(mid+1,r,s2,pla);}update(pla);
    34 }void reset(int rot){
    35     int f=tree[rot].fa;inr=0;redfs(rot);rebuild(1,inr,((root==rot)?root=mem[inm--]:mem[inm--]),f);
    36     for(;tree[rot].fa;rot=tree[rot].fa)update(tree[rot].fa);
    37 }char need_to_reset(int now){return tree[now].siz*3<tree[tree[now].son[0]].siz*4
    38     ||tree[now].siz*3<tree[tree[now].son[1]].siz*4||tree[now].siz<tree[now].hid*3;
    39 }void insert(int now,int x,int &ntr){if(!now){int u=(inm?mem[inm--]:++ins);
    40     tree[u].siz=tree[u].num=1;tree[u].val=x;root=u;return;}
    41     if(tree[now].val==x){if(tree[now].id) tree[now].id=0,tree[now].hid--;
    42         tree[now].siz++,tree[now].num++;if(need_to_reset(now)) ntr=now;return;
    43     }if(tree[now].son[tree[now].val<x]) insert(tree[now].son[tree[now].val<x],x,ntr);
    44     else{int u=(inm?mem[inm--]:++ins);tree[u].siz=tree[u].num=1;tree[u].fa=now;
    45         tree[u].val=x;tree[now].son[tree[now].val<x]=u;
    46     }update(now);if(need_to_reset(now)) ntr=now;
    47 }void delet(int now,int x,int &ntr){
    48     if(tree[now].val==x){if(tree[now].num) tree[now].num--,tree[now].siz--,tree[now].hid++;
    49         if(!tree[now].num) tree[now].id=1;if(need_to_reset(now)&&(tree[now].son[0]||tree[now].son[1])) ntr=now;return;
    50     }if(tree[now].son[tree[now].val<x]) delet(tree[now].son[tree[now].val<x],x,ntr);
    51     update(now);if(need_to_reset(now)) ntr=now;
    52 }int findran(int x){int u=root,ans=1;
    53     for(;u&&tree[u].val!=x;u=tree[u].son[x>tree[u].val]) ans+=(x>tree[u].val)*(tree[tree[u].son[x<tree[u].val]].siz+tree[u].num);
    54     if(u) ans+=tree[tree[u].son[0]].siz;return ans;
    55 }int findnum(int x){int u=root;
    56     for(char t;tree[u].son[tree[tree[u].son[0]].siz+tree[u].num<x?1:0]&&(tree[tree[u].son[0]].siz>=x||tree[tree[u].son[0]].siz+tree[u].num<x);
    57     t=tree[tree[u].son[0]].siz+tree[u].num<x,x-=t*(tree[tree[u].son[0]].siz+tree[u].num),u=tree[u].son[t]);return tree[u].val;
    58 }int las(int x){int rnk=findran(x);return findnum(rnk-1);}
    59 int nex(int x){int u=root,ans=0;
    60     for(;u&&tree[u].val!=x;u=tree[u].son[x>tree[u].val]) ans+=(x>tree[u].val)*(tree[tree[u].son[x<tree[u].val]].siz+tree[u].num);
    61     if(u) ans+=tree[tree[u].son[0]].siz+tree[u].num;return findnum(ans+1);
    62 }int main(){
    63     read(n);
    64     fui(i,1,n,1){
    65         int opt,x,y=0;read(opt);read(x);
    66         switch(opt){
    67             case 1:insert(root,x,y);if(y) reset(y);break;
    68             case 2:delet(root,x,y);if(y) reset(y);break;
    69             case 3:cout<<findran(x)<<endl;break;
    70             case 4:cout<<findnum(x)<<endl;break;
    71             case 5:cout<<las(x)<<endl;break;
    72             case 6:cout<<nex(x)<<endl;
    73         }
    74     }
    75     return 0;
    76 }
    AC代码
    60分代码

    注意以上两份代码只有insert函数和delet函数不同。AC代码为递归式的,60分代码为循环式的。我估计是循环的时候无法维护每个节点的hid值(至于你说找到这个点后再循环回根update,本蒟蒻也打过,并且TLE,还是60分。。。)

    替罪羊树的其他作用

    直接上题吧

    洛谷P4278 带插入区间K小值/BZOJ3065 带插入区间K小值

    解法之一:平衡树套线段树(替罪羊树套主席树)(然而本蒟蒻不会。。。)

    (ps:这题基本别想在洛谷A掉。此题堪称洛谷最难题。在BZOJ上4个点共60s,洛谷上5个点各1s,目前没有一个人A掉)

    附:关于平衡树套线段树

    又是引进VFleaking大佬的(Orzzzzzz)

     

    作者:A星际穿越
    我的博客写得这么烂,应该不会有人想转载的吧
    如果要转载的话,请在文章显眼处标明作者和出处谢谢
  • 相关阅读:
    pow()函数结果强制转化为int造成误差的分析
    warning: ISO C++ forbids converting a string constant to 'char*' [-Wwrite-strings]
    博客园鼠标点击特效代码
    codeblocks更改颜色主题
    codeblocks1712设置中文
    SQl
    项目中nodejs包高效升级插件npm-check-updates
    正则表达式的整理(将金钱数变成带有千分位)
    从一个数组中过滤出另外一个数组中相关字段相等的数据
    IONIC3 打包安卓apk详细过程(大量图文)
  • 原文地址:https://www.cnblogs.com/Axjcy/p/9483297.html
Copyright © 2011-2022 走看看