一直觉得平衡树水很深,有机会想好好搞一搞,这里记下我大致总结的内容吧。
平衡树主要有以下几种类型:
1.严格保证深度:红黑树、AVL等。这类平衡树始终把深度维护在logn级别,并且每次也只用logn的时间维护自己的性质,因为每次操作都是严格logn,常数会比较小,但伴随的一般是树的形态限制较死,编写时需要考虑的情况可能较多,代码难度较大等。
2.均摊复杂度:Splay、替罪羊等。每次操作可能O(1)可能O(n),但这并不妨碍它们最终复杂度只有O(nlogn),依据一般是复杂的势能分析之类,不深入研究的话只要记住它们效率可以保证就行了。此类平衡树无法可持久化,强行可持久化会导致无法均摊,复杂度退化。(替罪羊可以部分可持久化(不修改历史版本))
3.玄学即正解:Treap、跳表(假装是平衡树)等。用上一些随机策略,最后每次期望O(logn),随机的思想不仅几乎卡不掉,而且不像其他类平衡树为保持平衡加上了很多限制,这也使得没有太多限制的随机化平衡树充满待挖掘的潜力。
4.根本不平衡:普通BST、Spaly等。快利用Trajan大神发明的Spaly算法O(1)维护平衡树。
下面分别介绍这些平衡树(加粗的是在目前信息学竞赛中较为重要的)
介绍的时候顺便贴下模板,因为如果都是字有点吓人……
模板题->n个操作,支持插入一个数和查找k大(n<=500,000)。数据大部分随机,小部分特殊构造。给出大致的运行时间,仅供参考。
[基于模板题的效率对比] (可能由于我写的丑/问题特殊性/随机数据/随机种子生成器/评测机状态等原因产生一定误差,仅供参考)(还有缺的以后有机会再补)
替罪羊树(504ms) < AVL树(588ms) < 普通Treap(688ms) < 无旋Treap(1000ms) < 双旋Splay(1220ms) < 混合旋Splay(1384ms) < 随机合并树(TLE) < 单旋Spaly(TLE) < 裸BST(TLE)
可以看到替罪羊树的效率十分出色(可能是因为主要是随机数据),AVL和Treap紧随其后,无旋Treap和Splay功能强大但常数较大,后面纯属娱乐。
这里先贴个本题目前的Rank1 写的是权值线段树ZKW版 240ms 感觉已经接近极限了,还可以调整的就是可以把排序换成O(n)的
#include<cstdio> #include<algorithm> using namespace std; #define r register int char B[1<<26],*S=B,C;int X; inline int read() { while((C=*S++)<'0'||C>'9'); for(X=C-'0';(C=*S++)>='0'&&C<='9';)X=(X<<3)+(X<<1)+C-'0'; return X; } #define MN 500000 #define N 524288 int x[MN+5],y[MN+5],t[N*2+5],c[MN+5],rk[MN+5]; bool cmp(int a,int b){return y[a]<y[b];} void add(r k){for(k+=N;k;k>>=1)++t[k];} int find(r k){for(r i=1;i<<=1;){if(t[i]<k)k-=t[i++];if(i>N)return i-N;}} int main() { fread(B,1,1<<26,stdin); r n=read(),i; for(i=1;i<=n;++i)x[i]=read(),y[i]=read(),c[i]=i; sort(c+1,c+n+1,cmp); for(i=1;i<=n;++i)rk[c[i]]=i; for(i=1;i<=n;++i)x[i]?(add(rk[i]),0):printf("%d\n",y[c[find(y[i])]]); }
正文
红黑树:好像性质比较复杂,我也不是很懂,据说效率是目前各类平衡树中最高的,代码也是最难写的。我还不是很懂,有机会再补。
AVL树:我最开始学的就是AVL,因为很好理解,听说这玩意儿代码很长,但我一点都不觉得(我可能学的假的)。性质是任意节点左右子树深度差不超过1,不满足就旋一旋,要考虑各种情况保证旋完满足性质,用一些代码小技巧写起来还是挺短的。后来发现这种平衡树只能做最基本的操作,赶紧跳坑Splay。
#include<cstdio> #include<algorithm> using namespace std; char B[1<<26],*S=B,C;int X; inline int read() { while((C=*S++)<'0'||C>'9'); for(X=C-'0';(C=*S++)>='0'&&C<='9';)X=(X<<3)+(X<<1)+C-'0'; return X; } #define MN 500000 #define rtf MN+1 #define L(x) c[x][0] #define R(x) c[x][1] #define rt L(rtf) int fa[MN+5],c[MN+5][2],s[MN+5],h[MN+5],z[MN+5],tn; inline void update(int x){s[x]=s[c[x][0]]+s[c[x][1]]+1;h[x]=max(h[c[x][0]],h[c[x][1]])+1;} void rotate(int x) { int f=fa[x],ff=fa[f],l=R(f)==x,r=l^1; fa[f]=x;fa[x]=ff;fa[c[x][r]]=f; c[ff][R(ff)==f]=x;c[f][l]=c[x][r];c[x][r]=f; update(f);update(x); } void ins(int f,int t,int x) { if(!c[f][t]){fa[c[f][t]=++tn]=f;s[tn]=1;z[tn]=x;return;} f=c[f][t];ins(f,x>z[f],x); if(h[L(f)]-h[R(f)]>2){if(h[R(L(f))]>h[L(L(f))])rotate(R(L(f)));rotate(L(f));} else if(h[R(f)]-h[L(f)]>2){if(h[L(R(f))]>h[R(R(f))])rotate(L(R(f)));rotate(R(f));} update(f); } int find(int x,int k) { if(k<=s[L(x)])return find(L(x),k); if(k-=s[L(x)]+1)return find(R(x),k); return z[x]; } int main() { fread(B,1,1<<26,stdin); int n=read(),t,x; while(n--) { t=read();x=read(); if(t)ins(rtf,0,x); else printf("%d\n",find(rt,x)); } }
伸展树(Splay Tree):全国人民都在用。你啥平衡树都可以不会,但你不能不会Splay。OI中经常要弄一些区间翻转区间查询之类的,提取区间最方便最广为人知的应该就是Splay了吧。Splay基本思想就是每次访问树中节点都要把节点双旋旋到根上来(旋法百度都有比较详细的说明,这里不再赘述),即使一开始树不平衡,旋到根上树的结构就慢慢平衡了。一系列复杂度分析可以保证它是均摊nlogn的(单旋就没有任何保证),但是还是改变不了它常数巨大的缺点。其实有个隐藏的性质就是越高频访问同一些节点,Splay的效率就会越高(因为靠近根),这个性质在现实中可能有一些应用,但OI中难以用题目的形式表现出来(甚至还有人专门卡你)……Splay还是Link-Cut Tree的重要组件。Splay提取区间的方法是把左右端点旋到根附近,例如[l,r]就把l-1旋到根,r+1旋到根节点的右儿子,那么根节点右儿子的左儿子就是[l,r]区间,一般要建两个辅助点0和n+1来提取[1,n]。
#include<cstdio> char B[1<<26],*S=B,C;int X; inline int read() { while((C=*S++)<'0'||C>'9'); for(X=C-'0';(C=*S++)>='0'&&C<='9';)X=(X<<3)+(X<<1)+C-'0'; return X; } #define MN 500000 #define rt MN+1 int fa[MN+5],c[MN+5][2],s[MN+5],v[MN+5],tn; inline void update(int x){s[x]=s[c[x][0]]+s[c[x][1]]+1;} void rotate(int x) { int f=fa[x],ff=fa[f],l,r; l=c[f][1]==x;r=l^1; fa[f]=x;fa[x]=ff;fa[c[x][r]]=f; c[ff][c[ff][1]==f]=x;c[f][l]=c[x][r];c[x][r]=f; update(f); } void splay(int x,int r) { for(int f;fa[x]!=r;rotate(x)) if(fa[f=fa[x]]!=r)rotate(c[fa[f]][0]==f^c[f][1]==x?f:x); update(x); } void ins(int f,int t,int z) { if(!c[f][t]){fa[c[f][t]=++tn]=f;v[tn]=z;splay(tn,rt);return;} ++s[f=c[f][t]]; ins(f,z>v[f],z); } int find(int x,int k) { if(k<=s[c[x][0]])return find(c[x][0],k); if(k-=s[c[x][0]]+1)return find(c[x][1],k); splay(x,rt);return x; } int main() { fread(B,1,1<<26,stdin); int n=read(),t,x; while(n--) { t=read();x=read(); if(t)ins(rt,0,x); else printf("%d\n",v[find(c[rt][0],x)]); } }
自己瞎写的混合旋Splay
#include<cstdio> char B[1<<26],*S=B,C;int X; inline int read() { while((C=*S++)<'0'||C>'9'); for(X=C-'0';(C=*S++)>='0'&&C<='9';)X=(X<<3)+(X<<1)+C-'0'; return X; } #define MN 500000 #define rt MN+1 int fa[MN+5],c[MN+5][2],s[MN+5],v[MN+5],tn; inline void update(int x){s[x]=s[c[x][0]]+s[c[x][1]]+1;} void rotate(int x) { int f=fa[x],ff=fa[f],l,r; l=c[f][1]==x;r=l^1; fa[f]=x;fa[x]=ff;fa[c[x][r]]=f; c[ff][c[ff][1]==f]=x;c[f][l]=c[x][r];c[x][r]=f; update(f); } void splay(int x,int r) { for(int f;fa[x]!=r;rotate(x)) if(fa[f=fa[x]]!=r&&c[fa[f]][0]==f^c[f][1]==x)rotate(f); update(x); } void ins(int f,int t,int z) { if(!c[f][t]){fa[c[f][t]=++tn]=f;v[tn]=z;splay(tn,rt);return;} ++s[f=c[f][t]]; ins(f,z>v[f],z); } int find(int x,int k) { if(k<=s[c[x][0]])return find(c[x][0],k); if(k-=s[c[x][0]]+1)return find(c[x][1],k); splay(x,rt);return x; } int main() { fread(B,1,1<<26,stdin); int n=read(),t,x; while(n--) { t=read();x=read(); if(t)ins(rt,0,x); else printf("%d\n",v[find(c[rt][0],x)]); } }