替罪羊树作为平衡树家族里比较简单的一员,效率还是很不错的
只要不是维护序列之类的需要提取子树进行操作的问题,选择高效率的重量平衡树是无可非议的
我们可以用一个标准:需不需要采用旋转操作来对重量平衡树进行一个简单的分类:
没有采用旋转机制的有:跳表和替罪羊树
采用旋转机制的有:Treap
所有采用旋转机制的平衡树都有这么一个弊端:在平衡树的每个节点上维护一个集合,来存储子树内部所有的数,此时单次旋转操作可能有O(n)的时间复杂度
那么什么是重量平衡树?如果你把势能的概念引入到平衡树的节点上面去,就比较容易理解了
在这种情况下,完全二叉树的势能是最低的,对于那些势能高的子树,我们或旋转或拍平重构来使它接近甚至成为完全二叉树结构,这应该就是重量平衡树的本质了(其实吧,平衡树的话,应该都是这样的)
然后开始介绍正题:替罪羊树的平衡机理:
对于某个0.5<=alpha<=1满足size(lch(x))<=alpha*size(x)并且size(rch(x))<=alpha*size(x),即这个节点的两棵子树的size都不超过以该节点为根的子树的size,那么就称这个子树(或节点)是平衡的
然后如果不平衡的话,直接拍平之后重构为完全二叉树就好了
然后开始说代码,我的数据结构的代码风格,总体来说还是比较凌乱的,后期一定会修整的,一定会修整的。
const int INF=1000000000; const int maxn=2000005; const double al=0.75; int n; struct Tree { int fa; int size; int num; int ch[2]; }t[maxn]; int cnt; int root; int node[maxn]; int sum;
在这里al就是平衡因子,size是该子树包含的节点个数,num是值(Splay中我用的是v),cnt记录节点个数,node是个存点坐标的临时数组,其下标用sum来指代
然后再来说一说建树操作,一般我们的建树操作是指给定一个装满了数的数组,然后调用建树函数,以这个数组为依托递归建树,就像这个函数:
int build(int l,int r) { if(l>r) return 0; int mid=(l+r)/2; int x=node[mid]; t[t[x].ch[0]=build(l,mid-1)].fa=x; t[t[x].ch[1]=build(mid+1,r)].fa=x; t[x].size=t[t[x].ch[0]].size+t[t[x].ch[1]].size+1; return x; }
但其实为了方便,我们大可直接一个一个数往数据结构里面插就好了
这种操作在替罪羊树中只用于在重构子树的时候临时存一下子树拍平之后的那些点,我们引出插入函数:
void insert(int x) { int o=root; int cur=++cnt; t[cur].size=1; t[cur].num=x; while(1) { t[o].size++; bool son=(x>=t[o].num); if(t[o].ch[son]) o=t[o].ch[son]; else { t[t[o].ch[son]=cur].fa=o; break; } } int flag=0; for(int i=cur;i;i=t[i].fa) if(!balance(i)) flag=i; if(flag) rebuild(flag); }
可以看到如果插入之后不平衡了,就要完成重构了,重构子树为完全二叉树
这里给出判断是否需要重构的函数,就是本文开头给出的那个公式
bool balance(int x) { return (double)t[x].size*al>=(double)t[t[x].ch[0]].size &&(double)t[x].size*al>=(double)t[t[x].ch[1]].size; }
如果真的需要重构的话,就调用重构函数进行重构,重构操作是拍成链然后重建子树,之后还接回去
void rebuild(int x) { sum=0; recycle(x); int fa=t[x].fa; int son=(t[t[x].fa].ch[1]==x); int cur=build(1,sum); t[t[fa].ch[son]=cur].fa=fa; if(x==root) root=cur; }
这里给出拍成链的函数,这里就用到刚才说的那个node和sum了
void recycle(int x) { if(t[x].ch[0]) recycle(t[x].ch[0]); node[++sum]=x; if(t[x].ch[1]) recycle(t[x].ch[1]); }
插入的问题说完了,然后说说删除,其实替罪羊这个名字就是因为这个删除操作而来的
void erase(int x) { if(t[x].ch[0]&&t[x].ch[1]) { int cur=t[x].ch[0]; while(t[cur].ch[1]) cur=t[cur].ch[1]; t[x].num=t[cur].num; x=cur; } int son=(t[x].ch[0])?t[x].ch[0]:t[x].ch[1]; int k=(t[t[x].fa].ch[1]==x); t[t[t[x].fa].ch[k]=son].fa=t[x].fa; for(int i=t[x].fa;i;i=t[i].fa) t[i].size--; if(x==root) root=son; }
替罪羊树中的删除操作,就是用被删除节点的左子树的最后一个节点或者右子树的第一个节点来顶替被删除节点的位置
查询操作的话,具备一般平衡树的一些基本功能:
查询x数的排名
查询排名为x的数(这个Splay里面写了,剩下两个都没有写,以后再完善吧)
求x的前驱和后继
int get_rank(int x) { int o=root,ans=0; while(o) { if(t[o].num<x) ans+=t[t[o].ch[0]].size+1,o=t[o].ch[1]; else o=t[o].ch[0]; } return ans; } int get_kth(int x) { int o=root; while(1) { if(t[t[o].ch[0]].size==x-1) return o; else if(t[t[o].ch[0]].size>=x) o=t[o].ch[0]; else x-=t[t[o].ch[0]].size+1,o=t[o].ch[1]; } return o; } int get_front(int x) { int o=root,ans=-INF; while(o) { if(t[o].num<x) ans=max(ans,t[o].num),o=t[o].ch[1]; else o=t[o].ch[0]; } return ans; } int get_behind(int x) { int o=root,ans=INF; while(o) { if(t[o].num>x) ans=min(ans,t[o].num),o=t[o].ch[0]; else o=t[o].ch[1]; } return ans; }
其实查询操作对各个树而言大同小异,只不过,我目前知道的,Splay干什么都要splay到根节点一下子
最后,我们给出替罪羊树的完整实现:
1 #include<iostream> 2 #include<algorithm> 3 using namespace std; 4 const int INF=1000000000; 5 const int maxn=2000005; 6 const double al=0.75; 7 int n; 8 struct Tree 9 { 10 int fa; 11 int size; 12 int num; 13 int ch[2]; 14 }t[maxn]; 15 int cnt; 16 int root; 17 int node[maxn]; 18 int sum; 19 bool balance(int x) 20 { 21 return (double)t[x].size*al>=(double)t[t[x].ch[0]].size 22 &&(double)t[x].size*al>=(double)t[t[x].ch[1]].size; 23 } 24 void recycle(int x) 25 { 26 if(t[x].ch[0]) 27 recycle(t[x].ch[0]); 28 node[++sum]=x; 29 if(t[x].ch[1]) 30 recycle(t[x].ch[1]); 31 } 32 int build(int l,int r) 33 { 34 if(l>r) 35 return 0; 36 int mid=(l+r)/2; 37 int x=node[mid]; 38 t[t[x].ch[0]=build(l,mid-1)].fa=x; 39 t[t[x].ch[1]=build(mid+1,r)].fa=x; 40 t[x].size=t[t[x].ch[0]].size+t[t[x].ch[1]].size+1; 41 return x; 42 } 43 void rebuild(int x) 44 { 45 sum=0; 46 recycle(x); 47 int fa=t[x].fa; 48 int son=(t[t[x].fa].ch[1]==x); 49 int cur=build(1,sum); 50 t[t[fa].ch[son]=cur].fa=fa; 51 if(x==root) 52 root=cur; 53 } 54 void insert(int x) 55 { 56 int o=root; 57 int cur=++cnt; 58 t[cur].size=1; 59 t[cur].num=x; 60 while(1) 61 { 62 t[o].size++; 63 bool son=(x>=t[o].num); 64 if(t[o].ch[son]) 65 o=t[o].ch[son]; 66 else 67 { 68 t[t[o].ch[son]=cur].fa=o; 69 break; 70 } 71 } 72 int flag=0; 73 for(int i=cur;i;i=t[i].fa) 74 if(!balance(i)) 75 flag=i; 76 if(flag) 77 rebuild(flag); 78 } 79 int get_num(int x) 80 { 81 int o=root; 82 while(1) 83 { 84 if(t[o].num==x) 85 return o; 86 else 87 o=t[o].ch[t[o].num<x]; 88 } 89 } 90 void erase(int x) 91 { 92 if(t[x].ch[0]&&t[x].ch[1]) 93 { 94 int cur=t[x].ch[0]; 95 while(t[cur].ch[1]) 96 cur=t[cur].ch[1]; 97 t[x].num=t[cur].num; 98 x=cur; 99 } 100 int son=(t[x].ch[0])?t[x].ch[0]:t[x].ch[1]; 101 int k=(t[t[x].fa].ch[1]==x); 102 t[t[t[x].fa].ch[k]=son].fa=t[x].fa; 103 for(int i=t[x].fa;i;i=t[i].fa) 104 t[i].size--; 105 if(x==root) 106 root=son; 107 } 108 int get_rank(int x) 109 { 110 int o=root,ans=0; 111 while(o) 112 { 113 if(t[o].num<x) 114 ans+=t[t[o].ch[0]].size+1,o=t[o].ch[1]; 115 else 116 o=t[o].ch[0]; 117 } 118 return ans; 119 } 120 int get_kth(int x) 121 { 122 int o=root; 123 while(1) 124 { 125 if(t[t[o].ch[0]].size==x-1) 126 return o; 127 else if(t[t[o].ch[0]].size>=x) 128 o=t[o].ch[0]; 129 else 130 x-=t[t[o].ch[0]].size+1,o=t[o].ch[1]; 131 } 132 return o; 133 } 134 int get_front(int x) 135 { 136 int o=root,ans=-INF; 137 while(o) 138 { 139 if(t[o].num<x) 140 ans=max(ans,t[o].num),o=t[o].ch[1]; 141 else 142 o=t[o].ch[0]; 143 } 144 return ans; 145 } 146 int get_behind(int x) 147 { 148 int o=root,ans=INF; 149 while(o) 150 { 151 if(t[o].num>x) 152 ans=min(ans,t[o].num),o=t[o].ch[0]; 153 else 154 o=t[o].ch[1]; 155 } 156 return ans; 157 } 158 int main() 159 { 160 cnt=2; 161 root=1; 162 t[1].num=-INF,t[1].size=2,t[1].ch[1]=2; 163 t[2].num=INF,t[2].size=1,t[2].fa=1; 164 cin>>n; 165 int tmp,x; 166 for(int i=1;i<=n;i++) 167 { 168 cin>>tmp>>x; 169 if(tmp==1) 170 insert(x); 171 if(tmp==2) 172 erase(get_num(x)); 173 if(tmp==3) 174 cout<<get_rank(x)<<endl; 175 if(tmp==4) 176 cout<<t[get_kth(x+1)].num<<endl; 177 if(tmp==5) 178 cout<<get_front(x)<<endl; 179 if(tmp==6) 180 cout<<get_behind(x)<<endl; 181 } 182 }
替罪羊树与同类的平衡树相比,对查询操作具有极大的优势(重量平衡树的特性,当然对于那些维护序列的操作,重量平衡树没有用武之地)
而且实验证明在某种程度上是最快的?其实应该是因题而异
那么这个树的真实应用是啥呢?
替罪羊树可以优化无法旋转的树形结构的时间复杂度,即树套树
第一种情况是与K-D树的嵌套,第二种情况是与线段树的嵌套
平衡树套线段树通常不可写,因为这样嵌套之后平衡树无法旋转,但是用替罪羊树套函数式线段树是没有任何问题的
在介绍树套树部分时,将着重介绍这部分内容