在刷了許多道平衡樹的題之后,對平衡樹有了較為深入的理解,在這里和大家分享一下,希望對大家學習平衡樹能有幫助。
平衡樹有好多種,比如treap,splay,紅黑樹,STL中的set。在這里只介紹幾種常用的:treap、splay和替罪羊樹(其中treap包括旋轉treap和非旋轉treap)。
一、treap
treap這個詞是由tree和heap組合而成,意思是樹上的的堆(其實就是字面意思啦qwq)。treap可以說是由二叉搜索樹(BST)進化而來,二叉搜索樹每個點滿足它左子樹中所有點權值都比它小,它右子樹中所有點權值都比它大,這樣二叉搜索樹的中序遍歷出來的序列權值就是從小到大有順序的。對於一棵完全二叉搜索樹,查詢每個點的時間復雜度是O(logn)。但二叉搜索樹很容易就會退化成一條鏈(順序或逆序插入所有點),這樣它就失去了原有的作用,於是便有了treap,treap就是在維護BST性質的同時還要維護小根堆(其實大根堆也可以)的性質——每個點的另一個權值比它所有子樹上節點的都小,那么這個權值是什么呢?自然是隨機數了!只有隨機數才能使它成為一棵平衡樹(層數在logn層左右),因為每個點賦值都是隨機數,所以對於兩個數來說一個點比另一個點大的概率相等。那么怎么同時維護這兩種數據結構的性質呢?由此就產生了旋轉treap和非旋轉treap(具體原理下面再講)。
treap作為一種平衡樹,既可以維護集合,也可以維護序列(splay也同樣)。這兩者有什么區別呢?維護集合的treap的每個點的權值(具體地說是維護BST性質的權值)是集合中每個數的具體數值,但維護序列的treap的每個點的權值是序列中每個數的下標(也就是這個數在序列中的位置),而這個數具體是什么不影響平衡樹的結構,只是在求解時需要的一個數值。一般維護序列的題剛開始都會先給你一個序列,而維護集合的題每個數都是在過程中插入平衡樹中的。
1、旋轉treap
旋轉treap維護BST和堆的性質是靠旋轉實現的,旋轉只有兩種:左旋和右旋。如圖所示。
因為在插入或刪除一個數時可能會在樹中添加或減掉一個點,所以有可能使treap的性質(具體來說是堆的性質,因為插入時保證按BST性質來插入)不滿足或改變樹的結構,這時就要用旋轉操作來再次恢復treap的性質。旋轉treap在維護集合插入時可以把相同權值的的數放在同一個點,也可以建立不同的點來存,如何存要因題而異。
介紹旋轉treap的幾種常見操作(以相同權值放在同一個點為例):
變量聲明:size[x],以x為根節點的子樹大小;ls[x],x的左兒子;rs[x],x的右子樹;r[x],x節點的隨機數;v[x],x節點的權值;w[x],x節點所對應的權值的數的個數。
1)左旋和右旋
以上圖為例,左旋即把Q旋到P的父節點,右旋即把P旋到Q的父節點。
以右旋為例:因為Q>B>P所以在旋轉之后還要滿足平衡樹性質所以B要變成Q的左子樹。在整個右旋過程中只改變了B的父節點,P的右節點和父節點,Q的左節點的父節點,與A,B,C的子樹無關。
void rturn(int &x) { int t; t=ls[x]; ls[x]=rs[t]; rs[t]=x; size[t]=size[x]; up(x); x=t; } void lturn(int &x) { int t; t=rs[x]; rs[x]=ls[t]; ls[t]=x; size[t]=size[x]; up(x); x=t; }
2)查詢
我們以查詢權值為x的點為例,從根節點開始走,判斷x與根節點權值大小,如果x大就向右下查詢,比較x和根右兒子大小;如果x小就向左下查詢,直到查詢到等於x的節點或查詢到樹的最底層。
3)插入
插入操作就是遵循平衡樹性質插入到樹中。對於要插入的點x和當前查找到的點p,判斷x與p的大小關系,決定下一步走向p的左子樹還是右子樹。如果相同權值的數存入不同點的話,每次插入的點都會插在葉子結點下面。注意在插入后回溯時因為要保證堆的性質,所以要進行左旋或右旋。
void insert_sum(int x,int &i) { if(!i) { i=++tot; w[i]=size[i]=1; v[i]=x; r[i]=rand(); return ; } size[i]++; if(x==v[i]) { w[i]++; } else if(x>v[i]) { insert_sum(x,rs[i]); if(r[rs[i]]<r[i]) { lturn(i); } } else { insert_sum(x,ls[i]); if(r[ls[i]]<r[i]) { rturn(i); } } return ; }
4)上傳
每次旋轉后因為子樹有變化所以要修改父節點的子樹大小及一些平衡樹維護的信息。
void up(int x) { size[x]=size[rs[x]]+size[ls[x]]+w[x]; }
5)刪除
刪除節點的方法和堆類似,要把點旋到最下層再刪,如果一個節點w不是1那就把w--就行。
void delete_sum(int x,int &i) { if(i==0) { return ; } if(v[i]==x) { if(w[i]>1) { w[i]--; size[i]--; return ; } if((ls[i]*rs[i])==0) { i=ls[i]+rs[i]; } else if(r[ls[i]]<r[rs[i]]) { rturn(i); delete_sum(x,i); } else { lturn(i); delete_sum(x,i); } return ; } size[i]--; if(v[i]<x) { delete_sum(x,rs[i]); } else { delete_sum(x,ls[i]); } return ; }
推薦練習題:
2、非旋轉treap
非旋轉treap相對於旋轉treap更加簡單暴力一些,只要斷裂和合並兩個操作就能維護樹的平衡及所有操作(起碼我所知的所有操作qwq),它相對於旋轉treap能實現區間操作及可持久化且代碼簡短(對於我來說是不存在的QAQ)。
介紹一下這兩個操作:
1)斷裂
就是以一個點為界限,將平衡樹分裂成兩棵平衡樹。注意斷裂操作要保證左邊平衡樹中任意點權值小於右邊平衡樹中任意點的權值。
以將平衡樹分裂成權值<=val和權值>val兩部分為例:從根節點開始往下查找,當當前點權值<=val時,將當前點及它的右子樹接到分裂后第二棵平衡樹的左子樹上;反之則將當前點及它的左子樹接到分裂后第一棵平衡樹的右子樹上,直到找到葉子節點為止。
因為分裂后兩棵樹也要保證平衡樹的BST的性質,所以往第一棵樹上接節點只能往右子樹接,往第二棵樹上接節點只能往左子樹接。
void split(int x,int &lroot,int &rroot,int val) { if(!x) { lroot=rroot=0; return ; } if(v[x]<=val) { lroot=x; split(rs[x],rs[lroot],rroot,val); } else { rroot=x; split(ls[x],lroot,ls[rroot],val); } up(x); }
2)合並
合並操作和可並堆的合並類似,但可並堆是按左偏樹的左偏值來決定當前點是誰,而非旋轉treap是按隨機數來合並。
void merge(int &x,int a,int b) { if(!a||!b) { x=a+b; return ; } if(r[a]<r[b]) { x=a; merge(rs[x],rs[a],b); } else { x=b; merge(ls[x],a,ls[b]); } up(x); }
其他操作只要把treap斷裂開,對對應區間或點進行操作再合並回去就OK了。
這樣斷裂與合並為什么是對的?
從斷裂操作的過程我們可以觀察到,當這一次走左子樹時,將這個點的右子樹接到斷裂后的第二棵樹的左子樹上;而走右子樹時,則把這個點的左子樹接到第一棵樹的右子樹上。這樣保證了斷裂后兩棵樹的BST性質,即一個點的左子樹都小於它,右子樹都大於它,也保證了第一棵樹的所有點都小於第二顆樹的所有點。合並時將第一棵樹的右子樹和第二棵樹的左子樹合並,因為合並時按每個點賦值的隨機數來決定父子關系,所以保證了平衡性。而且合並時遵循第一棵樹右子樹的點只能是第二棵樹左子樹點的左兒子或第二棵樹左子樹的點只能是第一棵樹右子樹點的右兒子,所以合並后依舊保留了BST的性質。
可持久化:
非旋轉treap支持可持久化,可持久化也就是保留歷史版本,最暴力的方法就是每一次操作把歷史版本的treap復制一遍,然后再在新復制的treap上進行修改,但這樣空間時間顯然不夠。由於非旋轉treap沒有旋轉操作,所以每次操作最多遍歷logn個節點,其他節點沒有變動,所以只要把遍歷的這條鏈建出來即可。具體操作是在合並和分裂時每遍歷到一個節點就要新建一個點復制於當前遍歷到的點,然后對新建點再進行遞歸子樹。因為一次操作(例如插入和刪除)會多次合並及分裂,空間要遠遠比nlogn大,所以可以記錄每個點所屬版本,當一個點要復制於屬於同一版本的節點時就可以不再進行復制了,特別地對於區間操作如果需要下傳標記也要對標記下傳到的點新建點(詳情見可持久化文藝平衡樹代碼)。有人可能會問為什么不能在原版本直接下傳再復制?因為標記下傳到的點可能是更早版本的點,這樣就改變之前版本了,而可持久化需要保留歷史版本也就是不對歷史版本修改。總而言之就是只要對原版本樹的節點信息進行改變時就要新建節點。可持久化平衡樹的內存要遠遠大於其他可持久化數據結構,因此建議數組在不MLE的情況下盡量往大開。
updata:合並時可以不新建節點,因為合並前一定分裂過的,所以對於合並時每層遞歸到的點在分裂時一定遍歷過,而分裂與合並同屬一個版本,因此可以將合並時的復制節點操作略去。
附上可持久化普通平衡樹模板luoguP3835
#include<map> #include<set> #include<queue> #include<cmath> #include<cstdio> #include<cstring> #include<iostream> #include<algorithm> using namespace std; int n,m; int x,y; int cnt; int num; int r[25000000]; int v[25000000]; int ls[25000000]; int rs[25000000]; int root[8000010]; int size[25000000]; void updata(int x) { size[x]=size[ls[x]]+size[rs[x]]+1; } void build(int &x,int k) { x=++cnt; v[x]=k; size[x]=1; r[x]=rand(); } void copy(int x,int y) { r[x]=r[y]; v[x]=v[y]; ls[x]=ls[y]; rs[x]=rs[y]; size[x]=size[y]; } int merge(int a,int b) { if(!a||!b) { return a+b; } if(r[a]>r[b]) { int rt=++cnt; copy(rt,a); rs[rt]=merge(rs[rt],b); updata(rt); return rt; } else { int rt=++cnt; copy(rt,b); ls[rt]=merge(a,ls[rt]); updata(rt); return rt; } } void split(int now,int rt,int &x,int &y) { if(!now) { x=y=0; } else { if(v[now]<=rt) { x=++cnt; copy(x,now); split(rs[x],rt,rs[x],y); updata(x); } else { y=++cnt; copy(y,now); split(ls[y],rt,x,ls[y]); updata(y); } } } void del(int &rt,int v) { int x=0; int y=0; int z=0; split(rt,v,x,z); split(x,v-1,x,y); y=merge(ls[y],rs[y]); rt=merge(merge(x,y),z); } void insert(int &rt,int v) { int x=0; int y=0; int z=0; split(rt,v,x,y); build(z,v); rt=merge(merge(x,z),y); } int value(int rt,int k) { if(k==size[ls[rt]]+1) { return v[rt]; } else if(k<=size[ls[rt]]) { return value(ls[rt],k); } else { return value(rs[rt],k-size[ls[rt]]-1); } } int rank(int &rt,int v) { int x; int y; split(rt,v-1,x,y); int ans=size[x]+1; rt=merge(x,y); return ans; } int pre(int &rt,int v) { int x; int y; int k; int ans; split(rt,v-1,x,y); if(!x) { return -2147483647; } k=size[x]; ans=value(x,k); rt=merge(x,y); return ans; } int suf(int &rt,int v) { int x; int y; int ans; split(rt,v,x,y); if(!y) { return 2147483647; } else { ans=value(y,1); } rt=merge(x,y); return ans; } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) { scanf("%d%d%d",&m,&x,&y); root[i]=root[m]; if(x==1) { insert(root[i],y); } else if(x==2) { del(root[i],y); } else if(x==3) { printf("%d\n",rank(root[i],y)); } else if(x==4) { printf("%d\n",value(root[i],y)); } else if(x==5) { printf("%d\n",pre(root[i],y)); } else { printf("%d\n",suf(root[i],y)); } } }
及可持久化文藝平衡樹模板luoguP5055
#include<map> #include<queue> #include<stack> #include<cmath> #include<vector> #include<bitset> #include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #define ll long long using namespace std; ll ans; ll sum[10000000]; int r[10000000]; int ls[10000000]; int rs[10000000]; int v[10000000]; int s[10000000]; int size[10000000]; int num[10000000]; int a,b,c; int n,m; int opt; int x,y,z; int cnt; int root[2000010]; inline int copy(int x,int tim) { if(num[x]==tim) { return x; } int rt=++cnt; size[rt]=size[x]; sum[rt]=sum[x]; ls[rt]=ls[x]; rs[rt]=rs[x]; r[rt]=r[x]; s[rt]=s[x]; v[rt]=v[x]; num[rt]=tim; return rt; } inline void pushup(int rt) { size[rt]=size[ls[rt]]+size[rs[rt]]+1; sum[rt]=sum[ls[rt]]+sum[rs[rt]]+1ll*v[rt]; } inline void reverse(int &x,int tim) { if(!x) { return ; } x=copy(x,tim); swap(ls[x],rs[x]); s[x]^=1; } inline void pushdown(int rt,int tim) { if(s[rt]) { reverse(ls[rt],tim); reverse(rs[rt],tim); s[rt]^=1; } } inline int build(int val,int tim) { int rt=++cnt; num[rt]=tim; v[rt]=val; size[rt]=1; r[rt]=rand(); sum[rt]=1ll*v[rt]; return rt; } inline int merge(int x,int y,int tim) { if(!x||!y) { return x+y; } int rt; if(r[x]<r[y]) { rt=copy(x,tim); pushdown(rt,tim); rs[rt]=merge(rs[rt],y,tim); pushup(rt); return rt; } else { rt=copy(y,tim); pushdown(rt,tim); ls[rt]=merge(x,ls[rt],tim); pushup(rt); return rt; } } inline void split(int rt,int &x,int &y,int k,int tim) { if(!rt) { x=y=0; return ; } int now=copy(rt,tim); pushdown(now,tim); if(size[ls[now]]>=k) { y=now; split(ls[now],x,ls[y],k,tim); pushup(now); } else { x=now; split(rs[now],rs[x],y,k-size[ls[now]]-1,tim); pushup(now); } } inline void ins(int &rt,int k,int val,int tim) { split(rt,a,b,k,tim); rt=merge(merge(a,build(val,tim),tim),b,tim); } inline void del(int &rt,int k,int tim) { split(rt,a,b,k-1,tim); split(b,b,c,1,tim); rt=merge(a,c,tim); } inline void rotate(int &rt,int l,int r,int tim) { split(rt,b,c,r,tim); split(b,a,b,l-1,tim); reverse(b,tim); rt=merge(merge(a,b,tim),c,tim); } inline ll query(int &rt,int l,int r,int tim) { split(rt,b,c,r,tim); split(b,a,b,l-1,tim); ll res=sum[b]; rt=merge(merge(a,b,tim),c,tim); return res; } int main() { srand(12378); scanf("%d",&n); for(int i=1;i<=n;i++) { scanf("%d%d",&m,&opt); root[i]=root[m]; if(opt==1) { scanf("%d%d",&x,&z); x^=ans; z^=ans; ins(root[i],x,z,i); } else if(opt==2) { scanf("%d",&x); x^=ans; del(root[i],x,i); } else if(opt==3) { scanf("%d%d",&x,&y); x^=ans; y^=ans; rotate(root[i],x,y,i); } else { scanf("%d%d",&x,&y); x^=ans; y^=ans; ans=query(root[i],x,y,i); printf("%lld\n",ans); } } }
反向分裂:
在做題的過程中我發現有一些題目用非旋轉treap做不了。例如題目要求對編號為x的節點進行操作,這需要你以這個點為界限將平衡樹分裂,但你無法通過這個點的權值在平衡樹上找到這個點,說簡單點也就是你不知道這個點在平衡樹中具體在哪。比如:動態維護dfs序,因為插入一個點時會改變后面所有點的dfs序上位置,所以你不能通過dfs序上位置來在平衡樹上按size分裂。但splay就能輕松維護了,因為splay旋轉時采用的是非遞歸形式,也就是從操作點出發而不是從平衡樹的根節點出發。對比splay的操作方式,我想出了一種由操作點向根節點分裂的方法,也不知道有沒有別人也想出過這種方法,我就叫他反向分裂吧。其實就是將正常split反過來操作,多維護一個f[]數組記錄每個點的父親是誰,每次往上爬到父親,通過判斷當前點是父親的左子樹還是右子樹來決定將當前點的父節點接到分裂后第一棵平衡樹上還是第二棵平衡樹上。具體實現看代碼。
int merge(int x,int y) { if(!x||!y) { return x+y; } if(r[x]<r[y]) { rs[x]=merge(rs[x],y); f[rs[x]]=x; pushup(x); return x; } else { ls[y]=merge(x,ls[y]); f[ls[y]]=y; pushup(y); return y; } } void split(int rt,int &a,int &b) { int x=ls[rt]; int y=rs[rt]; ls[rt]=rs[rt]=0; pushup(rt); int now=rt; while(f[rt]) { if(ls[f[rt]]==rt) { ls[f[rt]]=y; f[y]=f[rt]; y=f[rt]; pushup(f[rt]); } else { rs[f[rt]]=x; f[x]=f[rt]; x=f[rt]; pushup(f[rt]); } rt=f[rt]; } f[x]=f[y]=0; f[now]=0; a=x; b=y; }
注意反向分裂不適用於節點有需下傳標記的情況,如果需要下傳標記那么依舊要記錄每個點的父節點,然后從操作點到根沿途打上一個標記再從根開始正常分裂即可。
推薦練習題:
二、笛卡爾樹:
對於一個給定序列建平衡樹的時間復雜度是O(nlogn)的,因為每次插入一個數是O(logn)的。這里介紹一種O(n)建樹的方法:笛卡爾樹。笛卡爾樹和treap很相似,都是同時具有二叉搜索樹和堆的性質,為了方便我們將二叉搜索樹權值稱為v1,堆權值稱為v2。以維護小根堆為例,在建樹時按照v1權值順序依次插入每個點,這樣保證當前點一定是插入到最右邊,我們用一個棧依次記錄從當前根往下的右子節點(即從根開始一直往右走的所有節點),依次將v2大於當前插入點的點彈棧,這樣就找到了第一個v2比當前插入點小的點p,將p的右子樹變為插入點的左子樹,然后將插入點變為p的右子樹即可。因為每個點只會入棧出棧一次,所以時間復雜度是O(n)的。
注意平衡樹建樹時要對於每個點pushup(上傳信息)。
#include<set> #include<map> #include<queue> #include<cmath> #include<stack> #include<cstdio> #include<vector> #include<bitset> #include<cstring> #include<iostream> #include<algorithm> #define ll long long using namespace std; int st[100010]; int v[100010]; int ls[100010]; int rs[100010]; int f[100010]; int top; int n; int root; int build() { st[top=1]=1; for(int i=2;i<=n;i++) { while(top&&v[st[top]]>v[i]) { top--; pushup(i); } if(top) { f[i]=st[top]; f[rs[st[top]]]=i; ls[i]=rs[st[top]]; rs[st[top]]=i; } else { f[st[1]]=i; ls[i]=st[1]; } st[++top]=i; } while(top) { pushup(st[top]); top--; } return st[1]; } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) { scanf("%d",&v[i]); } root=build(); }
三、替罪羊樹
替罪羊樹同樣是由一棵BST進化而來,不過它不像treap一樣需要隨機數來維護樹的平衡,它維護平衡的方法更加暴力一些:當對於一個點為根的子樹不平衡時就將這棵子樹拍扁重構。拍扁重構?就是求出這棵子樹的中序遍歷然后像線段樹一樣左右遞歸建樹。那么如何判斷一棵樹是否平衡呢?我們需要一個平衡因子,一般來說這個因子是0.7~0.9之間的一個浮點數。
重構部分代碼。
bool bad(int rt) { if(size[rt]*85<=100*max(size[ls[rt]],size[rs[rt]])) { return true; } return false; } void dfs(int rt) { if(!rt) { return ; } dfs(ls[rt]); if(sig[rt]) { q[++cnt]=rt; } dfs(rs[rt]); } void build(int &rt,int l,int r) { int mid=(l+r)>>1; rt=q[mid]; if(l==r) { ls[rt]=rs[rt]=0; size[rt]=tot[rt]=sig[rt]=1; return ; } if(l<mid) { build(ls[rt],l,mid-1); } else { ls[rt]=0; } build(rs[rt],mid+1,r); size[rt]=size[ls[rt]]+size[rs[rt]]+1; tot[rt]=tot[ls[rt]]+tot[rs[rt]]+1; } void rebuild(int &rt) { cnt=0; dfs(rt); if(cnt) { build(rt,1,cnt); } else { rt=0; } }
那么什么時候需要重構呢?當插入一個點時,可能會改變樹的平衡,因此在插入節點回溯時我們需要判斷回溯到的點的子樹是否平衡,如果不平衡就需要重構,但你會發現當前點是否重構不影響回溯到更上面的點對平衡的判斷,如果上面還有點需要重構,那么當前點就不用重構了。因此我們只記錄離根最近的需要重構的點並進行重構即可,因為重構會改變父子關系,所以建議維護父節點數組來方便重構。替罪羊樹的刪除有兩種方法:1、因為替罪羊樹沒有旋轉操作,所以當刪除一個點時就找到這個點的前驅或后繼來代替當前點的位置。2、還有一種刪除方式是對於要刪除的點不直接刪除,而是打上標記,當重構時再將被打標記的點丟棄。對於第一種刪除方式的重構與插入時重構類似,都是回溯時判斷。對於第二種刪除方式,我們記錄子樹實際剩余節點數和包括被打標記的待刪除點的節點數,如果這棵子樹刪除的點數太多了就不平衡了,這個判斷平衡方法同樣用平衡因子判斷。
因為重構后的樹高是logn的,所以對於單次操作的時間復雜度是O(logn),在重構時因為每個點只會被遍歷一次,而且重建樹時遞歸分治建樹,所以單次復雜度均攤O(logn)。
有人可能會疑惑:為什么平衡因子不設為0.5?因為我們知道重構需要遍歷整棵子樹,時間復雜度較高,雖然0.5能極大限度的保證查詢的時間復雜度但頻繁的重構是不划算的,所以0.7~0.9可以在保證查詢時間復雜度較為平衡的情況下,減少重構頻率,來最小化時間復雜度。
因為其他操作和旋轉treap類似,所以在這里就不一一講解了,直接附上普通平衡樹代碼。
#include<cmath> #include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #define ll long long using namespace std; int ls[400010]; int rs[400010]; int val[400010]; int size[400010]; int sig[400010]; int tot[400010]; int cnt; int q[400010]; int root; int n,x; int opt; int *point; int top; bool bad(int rt) { if(size[rt]*85<=100*max(size[ls[rt]],size[rs[rt]])) { return true; } return false; } void dfs(int rt) { if(!rt) { return ; } dfs(ls[rt]); if(sig[rt]) { q[++cnt]=rt; } dfs(rs[rt]); } void build(int &rt,int l,int r) { int mid=(l+r)>>1; rt=q[mid]; if(l==r) { ls[rt]=rs[rt]=0; size[rt]=tot[rt]=sig[rt]=1; return ; } if(l<mid) { build(ls[rt],l,mid-1); } else { ls[rt]=0; } build(rs[rt],mid+1,r); size[rt]=size[ls[rt]]+size[rs[rt]]+1; tot[rt]=tot[ls[rt]]+tot[rs[rt]]+1; } void rebuild(int &rt) { cnt=0; dfs(rt); if(cnt) { build(rt,1,cnt); } else { rt=0; } } void ins(int &rt,int k) { if(!rt) { rt=++top; val[rt]=k; size[rt]=1; tot[rt]=1; sig[rt]=1; return ; } size[rt]++; tot[rt]++; if(val[rt]>=k) { ins(ls[rt],k); } else { ins(rs[rt],k); } if(bad(rt)) { point=&rt; } } void clr(int &rt,int k) { if(sig[rt]&&size[ls[rt]]+1==k) { sig[rt]=0; size[rt]--; return ; } size[rt]--; if(size[ls[rt]]+sig[rt]>=k) { clr(ls[rt],k); } else { clr(rs[rt],k-size[ls[rt]]-sig[rt]); } } int rnk(int k) { int rt=root; int ans=1; while(rt) { if(val[rt]>=k) { rt=ls[rt]; } else { ans+=size[ls[rt]]+sig[rt]; rt=rs[rt]; } } return ans; } void del(int k) { clr(root,rnk(k)); if(tot[root]*95<=100*size[root]) { rebuild(root); } } int num(int k) { int rt=root; while(rt) { if(sig[rt]&&size[ls[rt]]+1==k) { return val[rt]; } else if(size[ls[rt]]>=k) { rt=ls[rt]; } else { k-=sig[rt]+size[ls[rt]]; rt=rs[rt]; } } } int main() { scanf("%d",&n); while(n--) { scanf("%d%d",&opt,&x); if(opt==1) { point=0; ins(root,x); if(point) { rebuild(*point); } } else if(opt==2){del(x);} else if(opt==3){printf("%d\n",rnk(x));} else if(opt==4){printf("%d\n",num(x));} else if(opt==5){printf("%d\n",num(rnk(x)-1));} else if(opt==6){printf("%d\n",num(rnk(x+1)));} } }
四、splay
splay的意思是延展樹,同樣滿足二叉搜索樹的性質,只不過splay維護平衡的方法只是旋轉。每次查詢會調整樹的結構,使被查詢頻率高的條目更靠近樹根。因此,就算剛開始時是一條鏈,在操作過程中也會變成正常的樹。
splay一共有六種旋轉方式,其中最基礎的兩種就是treap的那兩種,其他四種都是由那兩種演化來的。
基礎的旋轉只能向上轉一層,因此有了向上轉兩層的操作。但轉兩層自然不會那么簡單,旋轉是要有順序的,以上圖將x旋到g位置為例,要先將p選上去,再將x旋上去,也就是從上往下旋。雙選的方式也是splay保證時間復雜度的根本。
而像這種情況中將x旋到g位置,要先將x旋到p處,再旋到g處,也就是從下往上旋。
splay同樣可以實現區間操作且在LCT中會用到,但splay不能可持久化。對於單點操作只需把這個點旋到根節點再查詢有關信息即可,對於區間[x,y]操作,先將x-1旋到根節點,再將y+1旋到根節點的右兒子處,這樣根節點右兒子的左兒子就是想要的區間。那么如何旋到根節點呢?只要兩層兩層往上旋就好了。
splay並不是像treap那么平衡,它可能在某一時刻是一條鏈,但頻繁的旋轉使它能夠達到相對的平衡。splay在建樹時建議在最左端和最右端分別建立一個哨兵節點來代表全局最小值和最大值,但注意區間翻轉時不要把哨兵節點也跟着翻轉了。
最后附上splay區間操作代碼(以文藝平衡樹區間翻轉為例)
#include<cstdio> #include<algorithm> #include<iostream> #include<cmath> #include<cstring> using namespace std; int n,m; int root; int son[100007][3]; int size[100007]; int val[100007]; int f[100007]; int tag[100007]; int key[100007]; int sum[100007]; int d[100007]; int x,y; int total; int INF=1e9; int flag=0; bool get(int x) { return son[f[x]][1]==x; } void pushup(int x) { size[x]=size[son[x][0]]+size[son[x][1]]+1; } void pushdown(int x) { if(x&&tag[x]) { tag[son[x][0]]^=1; tag[son[x][1]]^=1; swap(son[x][0],son[x][1]); tag[x]=0; } } void rotate(int x) { int fa=f[x]; int anc=f[fa]; int k=get(x); pushdown(fa); pushdown(x); son[fa][k]=son[x][k^1]; f[son[fa][k]]=fa; son[x][k^1]=fa; f[fa]=x; f[x]=anc; if(anc) { son[anc][son[anc][1]==fa]=x; } pushup(fa); pushup(x); } void splay(int x,int goal) { for(int fa;(fa=f[x])!=goal;rotate(x)) { if(f[fa]!=goal) { rotate((get(fa)==get(x))?fa:x); } } if(!goal) { root=x; } } int build(int fa,int l,int r) { if(l>r) { return 0; } int mid=(l+r)>>1; int now=++total; key[now]=d[mid]; f[now]=fa; tag[now]=0; son[now][0]=build(now,l,mid-1); son[now][1]=build(now,mid+1,r); pushup(now); return now; } int rank(int x) { int now=root; while(1) { pushdown(now); if(x<=size[son[now][0]]) { now=son[now][0]; } else { x-=size[son[now][0]]+1; if(!x) { return now; } now=son[now][1]; } } } void turn(int l,int r) { l=rank(l); r=rank(r+2); splay(l,0); splay(r,l); pushdown(root); tag[son[son[root][1]][0]]^=1; } void write(int now) { pushdown(now); if(son[now][0]) { write(son[now][0]); } if(key[now]!=-INF&&key[now]!=INF) { if(flag==0) { printf("%d",key[now]); flag=1; } else { printf(" %d",key[now]); } } if(key[son[now][1]]) { write(son[now][1]); } } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) { d[i+1]=i; } d[1]=-INF; d[n+2]=INF; root=build(0,1,n+2); for(int i=1;i<=m;i++) { scanf("%d%d",&x,&y); turn(x,y); } write(root); return 0; }