\(\mathtt{Ps:}\) 由於作者實力不夠,寫錯的地方請海涵,可以提出,加以修改。
BST 二叉排序樹,有 左子樹所有點 \(<\) 根 \(<\) 右子樹所有點。
Splay:
伸展樹,也就是 Spaly ,可以通過伸展操作將 \(x\) 變成 \(y\) 的子結點,並不改變二叉排序樹的性質,其中 \(y\) 為 \(x\) 的祖先。
考慮 \(1\) 次操作,也就是將 \(x\) 變成 \(f(x)\) 。
旋轉操作:Rotate(x)
void Rotate(int x){
int fa=f[x],ffa=f[fa],m=Get(x),n=Get(fa);
Con(Son(x,m^1),fa,m);
Con(fa,x,m^1);
Con(x,ffa,n);
Update(fa);Update(x);
}
其中的 Con
為:
void Con(int x,int y,int z){
if(x){f[x]=y;}
if(y){Son(y,z)=x;}
}
其中的 Update
為:
void Update(int x){
if(x){
Siz(p)=1;
if(Son(x,0)){Siz(x)+=Siz(Son(x,0));}
if(Son(x,1)){Siz(x)+=Siz(Son(x,1));}
}
}
此時的 Update
不能省略,因為改變了樹的形態。
伸展操作: Splay(x,y)
void Splay(int x,int goal){
while(f[x]!=goal){
int fa=f[x];
if(f[fa]!=goal){Rotate(Get(fa)==Get(x)?fa:x);}
Rotate(x);
}
if(!goal){rt=x;}
}
這里是用雙旋實現的,不難發現,單次 Splay 復雜度為 \(O(\log n)\) 。
雙旋和單旋的區別,就在於上述代碼的第 4 行。
雙旋,可以保證 BST 接近於完全二叉樹,而單旋會變得十分混亂,復雜度不能保證。
注意如果 \(y=0\) ,即將 \(x\) 旋轉到根節點,那么要更新 rt
。
插入 \(x\) :根據 BST 的性質,從根節點往下新生成 1 個葉子節點
刪除 \(x\) :找到 \(x\) 對應的編號,將其旋轉到根節點,把左子樹接在 \(x\) 的后繼上,然后刪除 \(x\) 。
將 \(x-1\) 旋轉到根節點, \(x+1\) 旋轉到 \(x-1\) 的右子樹,那么 \(x\) 就在 \(x-1\) 的左子樹上。
\(x\) 的前驅 :找到 Son(x,0)
一直遞歸 x=Son(x,1)
。
\(x\) 的后繼:找到 Son(x,1)
一直遞歸 x=Son(x,0)
。
查找 \(x\) 的排名:根據 BST 的性質,從根節點遞歸。
當 x=Val(now)
則返回 now
;
當 x<Val(now)
遞歸右子樹 ;
當 x>Val(now)
遞歸左子樹 .
查找排名為 \(x\) 的數:與上述情況類似。
查找最接近 \(x\) 的大於 / 大於等於 / 小於等於 / 小於 \(x\) 的數:與上述情況類似。
Splay 還可以實現區間操作。
查找區間 \([l,r]\) :將 \(l-1\) 旋轉到根節點,\(r+1\) 旋轉到 \(l-1\) 的左子樹,那么 \([l,r]\) 就在 \(r+1\) 的左子樹上。
\(\mathtt{Ps:}\) 防止 \(l-1,r+1\) 不合法,可以插入 Inf
和 -Inf
避免,然后特判。
建樹:
要滿足 BST 的性質,需根據題目條件而改變。
查找第 \(k\) 大,排名第 \(k\) 的數等與值相關的操作,都需要按照值排序。
題目的操作與序列有關,那么就要按照序列排序。
\(\mathtt{Ps:}\) 這里所說的序列排序是符合題目要求之后的序列。
Build
與線段樹很相似,如用這種方法建樹,就要按照某個順序排序,值或者序列 。
int Build(int l,int r,int fa){
int mid=(l+r)>>1,now=++cnt;
f[now]=fa;Val(now)=d[mid];
Son(now,0)=Son(now,1)=0;
if(l<=mid-1){
Son(now,0)=Build(l,mid-1,now);
}
if(r>=mid+1){
Son(now,1)=Build(mid+1,r,now);
}
Update(now);
return now;
}
這里給出的是按照序列排序的代碼。
若要實現按照值排序,就在開始按照值排序即可。
Insert
就是 BST 獨有的,只需插入過程中,實現排序即可。
void Insert(int k){
if(!cnt){
cnt++;Siz(cnt)=Num(cnt)=1;Val(cnt)=k;
Son(cnt,0)=Son(cnt,1)=f[cnt]=0;
rt=cnt;
}
else{
int now=rt,fa=0;
while(1){
if(Val(now)==k){
Num(now)++;
Update(now);Update(fa);
Splay(now,0);
return;
}
fa=now;now=Son(now,k>Val(now));
if(!now){
cnt++;Siz(cnt)=Num(cnt)=1;Val(cnt)=k;
Son(cnt,0)=Son(cnt,1)=0;
Son(fa,k>Val(fa))=cnt;
f[cnt]=fa;
Splay(cnt,0);
return;
}
}
}
}
這里給出的是按照值排序的代碼。
若要實現按照序列排序的代碼,就只需要 now=Son(now,1)
即可。
\(\mathtt{Ps:}\) Insert
建樹每次插入 1 個節點都要 Splay ,不然有可能變成 1 條鏈。
能 Splay,就 Splay 。
對於區間操作,跟線段樹比較類似,也可以添加 Tag
標記。
其中 Splay
可能有些變化。
比如要實現區間加法:
void Splay(int x,int goal){
int len=0;
for(int i=x;i;i=f[i]){q[++len]=i;}
for(int i=len;i;--i){P_d(q[i]);}
while(f[x]!=goal){
int fa=f[x];
if(f[fa]!=goal){Rotate(Get(fa)==Get(x)?fa:x);}
Rotate(x);
}
if(!goal){rt=x;}
}
改變的是 2 ~ 4 行。
由於 Splay
會改變樹的形態,那么就要敢在操作前把標記下傳。
那為什么不 Update
呢?
因為此時 Update
沒用。
Rotate
會改變樹的形態,所以只有在 Rotate
之后再 Update
才游泳,這一點需要自己判斷。
如果不想判斷,也可以哪哪都加上 Update
Push_down
,只不過有時候常數會變得無比巨大。
對於不知道如何處理的操作,一般都可以將其旋轉到根節點,就迎刃而解了。
如果有多個操作該怎么辦?
可以分析它們之間的關系。
比如區間翻轉與區間加法毫無關聯,也就是互相不影響,先做哪個都可以。
比如區間加法與區間乘法,那么就可以假設先做加法,再做乘法,如何把不和法的乘法變成加法,反之,同理。
這里可以參考 [模板]線段樹2 。
無旋 Treap:
分裂操作:Split(rt,k,x,y)
將 以 rt
為根的數按把小於等於 \(k\) 的分離給 \(x\) ,把大於 \(k\) 的分離給 \(y\) ,變成以 \(x,y\) 為根的兩棵樹。
\(\mathtt{Ps:}\) 代碼不難理解,故沒有解釋。
void Split(int rt,int k,int &x,int &y){
if(!rt){x=y=0;return;}
else if(Val(rt)<=k){
x=rt;
Split(Son(rt,1),k,Son(rt,1),y);
}
else{
y=rt;
Split(Son(rt,0),k,x,Son(rt,0));
}
Update(rt);
}
這里的 Split
是按照權值分裂,其中 Update
同 Splay 。
合並操作:Merge(x,y)
將以 \(x,y\) 為根的兩棵樹合並成 1 棵樹,並返回根。
int Merge(int x,int y){
if(!x||!y){
return x+y;
}
else if(Rd(x)<Rd(y)){
Son(x,1)=Merge(Son(x,1),y);
Update(x);
return x;
}
else{
Son(y,0)=Merge(x,Son(y,0));
Update(y);
return y;
}
}
其中的 Rd(x)
為插入節點時給的隨機數,保證 Treap 的平衡。
-
插入 \(k\) :
Split(rt,k,x,y)
rt=Merge(Merge(x,New(k)),y)
-
刪除 \(k\) :
Split(rt,k,x,z)
`Split(rt,k-1,x,y)` rt=Merge(Merge(x,Merge(Son(y,0),Son(y,1))),y)
查找值的操作,跟 Splay 一模一樣。
無旋 Treap 也可以實現區間操作,但沒有 Splay 那么好用。
想要實現區間操作,就必須按照序列排序。
\([l,r]\) 的區間操作:Split(rt,l-1,x,y)
Split(rt,l-r+1,y,z)
其中以 \(y\) 為根的樹便是區間 \([l,r]\) 。
這里的 Split
是按照 Siz
分裂,原理與權值分裂相同。
\(\mathtt{Ps:}\) 如果操作會改變樹的形態,那么就要在其之前 Update
和 Push_down
Push_down
之后 Update
。