【學習筆記】淺談BST


\(\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:}\) 如果操作會改變樹的形態,那么就要在其之前 UpdatePush_down

Push_down 之后 Update


[CQOI2014]排序機械臂

[NOI2004]郁悶的出納員

送花

寶石管理系統

[HNOI2004]寵物收養場

[ZJOI2006]書架

[NOI2003]文本編輯器

序列終結者

[HNOI2002]營業額統計

資料來源:

【學習筆記】 Splay

【學習筆記】Treap

ModestCoder_


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM