替罪羊树
#include<cstdio> #include<ctime> #define mid(l,r) ((l+r)/2) #define max(a,b) (a>b?a:b) #define min(a,b) (a<b?a:b) #define nil 0 #define alpha 0.75f #define MXN 100000+3 int val[MXN],size[MXN],fa[MXN],left[MXN],right[MXN],recycle[MXN]; int root,ntop,rtop=-1,count,lst[MXN],cnt; int newnode(int k){ int nw; if(rtop!=-1) nw=recycle[rtop--]; else nw=++ntop; val[nw]=k; size[nw]=1; fa[nw]=nil; left[nw]=nil; right[nw]=nil; return nw; } bool check(int now){ if(size[left[now]]<=alpha*size[now]&&size[right[now]]<=alpha*size[now]) return false; return true; } void travel(int now){ if(now==nil) return; travel(left[now]); lst[cnt++]=now; travel(right[now]); return; } int build(int l,int r){ if(l>=r) return nil; int nw=lst[mid(l,r)]; left[nw]=build(l,mid(l,r)); right[nw]=build(mid(l,r)+1,r); fa[left[nw]]=nw; fa[right[nw]]=nw; size[nw]=size[left[nw]]+size[right[nw]]+1; return nw; } void insert(int &now,int ins){ if(root==nil) root=ins; else if(now==nil) now=ins; else{ size[now]++; if(val[now]>=val[ins]){ if(left[now]==nil){ left[now]=ins; fa[ins]=now; } else insert(left[now],ins); } else{ if(right[now]==nil){ right[now]=ins; fa[ins]=now; } else insert(right[now],ins); } } return; } void insert(int k){ int ins=newnode(k); insert(root,ins); int t=0,f,flag; for(int i=ins;fa[i]!=nil;i=fa[i]){ if(check(i)) t=i; } if(t){ cnt=0; f=fa[t]; if(left[f]==t) flag=0; else flag=1; travel(t); int a=build(0,cnt); if(t==root) root=a; else{ fa[a]=f; if(flag) right[f]=a; else left[f]=a; } } return; } void remove(int now,int k){ if(now==nil) return; if(val[now]==k){ if(left[now]==nil&&right[now]==nil){ if(left[fa[now]]==now) left[fa[now]]=nil; else right[fa[now]]=nil; fa[now]=nil; if(now==root) root=nil; recycle[++rtop]=now; return; } else if(left[now]==nil){ if(left[fa[now]]==now) left[fa[now]]=right[now]; else right[fa[now]]=right[now]; fa[right[now]]=fa[now]; fa[now]=nil; if(now==root) root=right[now]; recycle[++rtop]=now; return; } else if(right[now]==nil){ if(left[fa[now]]==now) left[fa[now]]=left[now]; else right[fa[now]]=left[now]; fa[left[now]]=fa[now]; fa[now]=nil; if(now==root) root=left[now]; recycle[++rtop]=now; return; } else{ int t=left[now]; while(right[t]!=nil) t=right[t]; val[now]=val[t]; size[now]--; remove(left[now],val[t]); return; } } else{ size[now]--; if(val[now]>k) remove(left[now],k); else remove(right[now],k); } return; } int search(int now,int k){ if(now==nil) return nil; if(val[now]==k) return now; if(val[now]>k) return search(left[now],k); if(val[now]<k) return search(right[now],k); } int select(int now,int k){ if(now==nil) return nil; if(size[left[now]]+1==k) return now; if(size[left[now]]+1>=k) return select(left[now],k); if(size[left[now]]+1<k) return select(right[now],k-size[left[now]]-1); } int rank(int now,int k){ if(now==nil) return 1; if(val[now]>=k) return rank(left[now],k); if(val[now]<k) return rank(right[now],k)+size[left[now]]+1; } int get_prev(int now,int k){ if(now==nil) return -0x6fffffff; if(val[now]<k) return max(val[now],get_prev(right[now],k)); else return get_prev(left[now],k); } int get_succ(int now,int k){ if(now==nil) return 0x6fffffff; if(val[now]>k) return min(val[now],get_succ(left[now],k)); else return get_succ(right[now],k); } int main(){ int n; scanf("%d",&n); root=nil; int p,x,a=1; while(n--){ scanf("%d%d",&p,&x); if(p==1) insert(x); if(p==2) remove(root,x); if(p==3) printf("%d\n",rank(root,x)); if(p==4) printf("%d\n",val[select(root,x)]); if(p==5) printf("%d\n",get_prev(root,x)); if(p==6) printf("%d\n",get_succ(root,x)); } return 0; }
替罪羊树是平衡树中的一股泥石流。平衡树的常规操作——旋转在这里毫无用武之地,取而代之的是拍扁重构。
首先我们可以想象一个简单的问题:最优的二叉搜索树的形状一定是一棵结构颇似线段树的树,它的高度是logn。
然后我们可以想一想写线段树时是怎么写的,是递归平分区间。
然后我们还可以通过二叉搜索树最普通的性质:中序遍历是一个有序的数列而得知这条性质的一种应用:对一个有序数列进行分治建树,可以获得一棵二叉搜索树。
以上三点是替罪羊树维护平衡的基本思路:当发现某一棵子树不平衡时,利用中序遍历获得一个有序数列,再利用类似于线段树的写法,将这棵树重新建为一棵完美的二叉树。显然,拍扁的复杂度为O(n),重构的复杂度为O(logn),总体复杂度为O(n)。
我们可能会问,这样不是和退化为链的复杂度一样吗?替罪羊树利用以下判断平衡的条件解决这项问题:
当size(left(x))<=α*size(x)且size(right(x))<=α*size(x),0.5<=α<=1时,子树平衡。显然当α=0.5时,这是一棵严格的左右子树大小相等的树。然而替罪羊树一般使α在0.7附近,称为宽松平衡,来减少重构操作,且使每次重构的规模不会过大。另一项操作是,重构时选择最大的不平衡子树来重构,以使整棵树尽量平衡,减少后续的重构次数。此时替罪羊树的均摊复杂度约等于O(logn)。这个可以证明,然而本人蒟蒻丝毫看不懂...
替罪羊树有两种可行的删除:一种是像正常二叉平衡树一样,一种是给这个节点打上标记,代表它被删除了,进行运算时完全不考虑它,等它所在子树重构时再真正删除。特别的,当被标记节点数超过一半时直接重构整棵树。
我用上一篇写的SBT和这个替罪羊树对比,替罪羊树与SBT时间耗费大体相当。
我还是比较喜欢平衡树这种结构的(^_^)ノ现在学习了三种平衡树,感觉替罪羊树像是一个人维护一根铁杆,铁杆弯曲了的话强行掰直;SBT像是在杠杆上左右摇晃,不断寻找平衡点;splay像是一个人单手托着装满豆子的盘子,发现不平衡,晃一晃就又平衡了...不过还有奇葩的2-3-4树,看图解感觉在有丝分裂...
替罪羊树思路很简单,学起来就比较容易,我自认代码比较清晰,因此没有过多解释。
PS:
一般情况下我写平衡树只有插入维护平衡(当然splay的话查找之类的也要),删除是不维护的。由于删除操作不会使树的高度变深,因此若只对一棵树进行删除和查找,明显复杂度不会变高。不过不清楚这样做是否会影响真实的复杂度。
PSS:
学了三种平衡树再去做题,我想说:只支持平衡的平衡树学太多貌似没什么用...还是要熟练地运用功能强大的Splay(尤其是Splay支持完全的区间操作并能应用于动态树),也要熟练地运用非旋Treap来解决可持久化问题(虽然动态树和非旋Treap我都没有学),大约这也是为什么除了这两种平衡树,其他的树在OI赛场上不常用的原因吧...