替罪羊树摘要
原理
替罪羊树不依靠旋转,而是依靠重构不平衡的子树使整棵树达到平衡状态。
变量介绍
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; //临时数组,重构用
各种操作
暴力重构
当一颗子树不平衡时,便将其重构,这是替罪羊树的核心思想。判断一颗子树是否平衡的方法有很多,我采用的方法是,若一颗子树的某个儿子子树的大小大于这颗子树的大小的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 }
重构有两种方法,我采用的是时空复杂度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 }
插入
和普通的平衡树一样插入,注意要用递归,并且传参的时候传一个引用,用来存回溯时最浅的需要被重构的子树根编号。
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 }
删除
替罪羊树的删除并不是真正意义上的删除,而是在这个数被删除消失时打上一个删除标记,当重构时再真正删除,或再次添加这个数来取消这个标记。(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 }
查询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 }
查询排名为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 }
查询x前驱
先找到这个数的排名y,再找到排名为y-1的数就是x的前驱。
1 int las(int x) { 2 int rnk=findran(x); //先找到这个点的排名 3 return findnum(rnk-1); //再找到比它小的最大的数 4 }
查询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 }
时空复杂度
时间复杂度
重构:单次重构的复杂度为O(n),但通过势能分析可以得出重构的均摊复杂度为O(logn)。推导过程来自VFleaking大佬的论文(Orzzzzzz)。
插入、删除、查询:由于保证树高均摊logn,因此复杂度为均摊O(logn)
常数还可以,似乎仅比红黑树慢,但单一的alpha容易被卡。因此可以开始就随机若干个alpha,取出合格的取平均值
空间复杂度
O(n)
例题
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 }
注意以上两份代码只有insert函数和delet函数不同。AC代码为递归式的,60分代码为循环式的。我估计是循环的时候无法维护每个节点的hid值(至于你说找到这个点后再循环回根update,本蒟蒻也打过,并且TLE,还是60分。。。)
替罪羊树的其他作用
直接上题吧
洛谷P4278 带插入区间K小值/BZOJ3065 带插入区间K小值
解法之一:平衡树套线段树(替罪羊树套主席树)(然而本蒟蒻不会。。。)
(ps:这题基本别想在洛谷A掉。此题堪称洛谷最难题。在BZOJ上4个点共60s,洛谷上5个点各1s,目前没有一个人A掉)
附:关于平衡树套线段树
又是引进VFleaking大佬的(Orzzzzzz)