替罪羊树摘要

原理

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

变量介绍

 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星际穿越
我的博客写得这么烂,应该不会有人想转载的吧
如果要转载的话,请在文章显眼处标明作者和出处谢谢
posted @ 2018-08-15 18:49  A星际穿越  阅读(343)  评论(2编辑  收藏  举报