1. 程式人生 > 實用技巧 >淺談 FHQ-Treap

淺談 FHQ-Treap

關於FHQ-Treap

            ——作者:BiuBiu_Miku

可能需要的前置知識:

   一.樹形結構的瞭解:

       樹形,顧名思義,就是像樹一樣有很多分叉口,而這裡以二叉樹為例子,二叉樹表示整棵樹每個節點的的分叉都小於或等於二,樹上最頂端的節點稱之為根節點,下面的稱為葉子節點。如圖(1)就是一棵二叉樹。

   二.平衡樹的概念:

      平衡樹也叫二叉查詢樹,學過OI大大佬們應該都知道一個東西叫做二分查詢,這裡叫二叉查詢,所以也具備一個性質,當前節點的左子樹上每個節點的值都小於父親節點(當前節點)的值,右子樹上每個節點的值都大於父親節點(當前節點)的值。如圖(2)就是一棵二叉查詢樹。

   三.關於平衡樹的一些基本操作的瞭解:

       1.插入(指的在樹中插入某個值)
       2.刪除(指刪除某個在樹中存在的值)
       3.查詢排名(一般指比自己小的數字的個數+1,從小到大)
       4.找第K小(指在樹中尋找排名為K的值是多少)
       5.前驅(指在樹中比某個值小且又是最接近這個值得數。如當前樹中假設有2,3,5,6四個數字,則5的前驅就是3)
       6.後繼(指在樹中比某個值大且又是最接近這個值得數。如當前樹中假設有2,3,5,6四個數字,則3的後繼就是5)

   三.treap:

       它是一種解決平衡樹基本操作的方法,它依靠旋轉來維護平衡(你不知道旋轉是什麼也行,你就理解為一種維護樹平衡的方法),同時我們不希望整棵樹變成一個鏈(圖(3)就是一棵變成鏈的樹),不然時間複雜度可想而知,所以他加了一個東西叫做修正值(不知道大佬們怎麼叫這玩意,反正我喜歡叫他修正值),來糾正樹,不讓他變成鏈,而這個修正值是隨機的,且整棵樹的修正值滿足一個大根堆或者小跟堆的性質,例如葉子節點的修正值永遠小於父親節點的修正值,不然就通過旋轉糾正樹。


進入正題
    眾所周知,關於實現平衡樹基本操作的方法有很多,例如splay,treap,紅黑樹,等等解法。
    其中,大部分都利用到了旋轉(左旋右旋),其程式碼可能讓一些大佬看得不適,因此,一位叫做範浩強的巨佬提出了一種不需要旋轉的treap,因此稱為FHQ-Treap。
    既然還是平衡樹,那麼它自然有自己的一套維護樹平衡(使樹仍然有著二叉查詢樹的性質)的方法。
    它的操作不是左旋右旋,而是分裂合併。

初始化:

    直接上程式碼吧!

struct Node
{
	long long val , size , fix ;        //val表示當前節點的值,size表示他的左子樹和右子樹的節點個數(包括自己),fix表示修正值
	long long lson , rson ;        //lson表示左兒子的編號,rson表示右兒子的編號        
} fhq [ 100006 ] ;
long long cur ;                              
long long root ;            //表示根節點
inline long long New_Node ( long long val )         /建一個新節點
{
	long long sd=1e6*2+5;      
	cur ++ ;                        //節點編號+1
	fhq [ cur ] . val = val ;                  //給當前節點的值賦值
	fhq [ cur ] . fix = rand() % sd ;            //為修正值生成一個隨機數
	fhq [ cur ] . size = 1 ;                     //當前節點沒有兒子,只有自己,所以初始為1
	return cur ;                  //返回當前節點編號
}
inline void update ( long long now )      //更新或維護節點
{
	fhq [ now ] . size = 0;           //先將當前節點的長度清0
	fhq [ now ] . size += fhq [ fhq [ now ] . lson ] . size ;         //根據定義加上左兒子和右兒子的長度
	fhq [ now ] . size += fhq [ fhq [ now ] . rson ] . size ;
	fhq [ now ] . size ++ ;      //包括自己也算在長度內
}

操作一:分裂

    顧名思義,分裂指的是把一棵樹按照某個值(我管他叫分裂值)分裂成兩棵樹,一般把這兩棵樹分別叫做x和y,x上的值都小於或等於這個值,y上的值都大於這個值。
    如圖(4)就是一棵樹的分裂。

    其原理很簡單就是搜尋整棵樹。
    關於x:如果找到一個值小於或等於分裂值時,就把它以及它的左子樹歸為x,然後往右子樹繼續尋找。那麼有的人就問了,為什麼就把它以及它的左子樹歸為x?明明只有x滿足啊!你想啊,平衡樹的性質不就是左子樹都小於根嗎,既然這個根都小於某個特定值了,那麼當前節點的左子樹不更加小於這個特定值嗎?那麼為什麼要往右子樹繼續搜呢?因為右子樹比根大,所以我們不能確定右子樹全部的值小於分裂值,因此要繼續找。
    關於y:就正好相反,如果找到一個值大於分裂值時,就把它以及它的右子樹歸為y,然後往左子樹繼續尋找。
    上程式碼!

inline void spilt ( long long now , long long val , long long &x , long long &y ) //其中now表示當前搜尋到的節點,val表示分裂值,x,y就是兩棵分裂的樹
{
	if( !now ) x = y = 0 ;      //這裡指,如果當now便利到一個不存在的節點的時候,x和y就沒有節點可以加上了,因此可以退出迴圈。                           
	else 
	{
		if(fhq [ now ] . val <= val)      //如果當前節點的值小於或等於分裂值,則把當前的值歸給x。
		{
			x = now ;            //歸入x
			spilt(fhq [ now ] . rson , val , fhq [ now ] . rson , y ) ;      //繼續往右子樹尋找
		}
		else      //同上,不過相反
		{
			y = now ;
			spilt(fhq [ now ] . lson , val , x , fhq [ now ] . lson ) ;
		}
		update(now);        //維護一下當前搜尋到的節點
	}
}

操作二:合併

    顧名思義,合併表示的就是把兩棵樹合併成一棵樹,並且合併之後,樹仍然符合二分查詢樹的性質且修正值滿足小跟堆或大根堆。我們這裡以大根堆為例子。
    如圖(5)就是兩棵樹的合併圖。

    那麼怎麼合併才能讓樹仍然符合二分查詢樹的性質且形成一個大根堆呢?
    你想啊,我們在分裂的時候x樹上所有的值是不是都小於y上所有的值因此,可以推測,y一定在x樹的右邊,根據大根堆,右可以判斷y在x的下邊,所以y在x樹的右下邊。
    因此,當我們符合大根堆性質的時候就把y樹合併到x樹上就行了(這裡不用管二叉搜尋樹的性質,因為本來x是一個合法的二叉搜尋樹,y也是一個合法的二叉搜尋樹,且x上所有值都小於y上所有值,因此只要合併時合法且不破壞x樹和y樹本身,就不會破壞性質)
    上程式碼!

inline long long merge ( long long &x , long long &y )                  //其中x和y表示要合併的兩棵樹
{
	if( !x || !y )return x + y ;                //這裡可以理解為如果有一個不存在就返回另一個的值(這個值表示把兩棵樹合併之後的根的編號)中間的加號也能換成|
	if( fhq [ x ] . fix > fhq [ y ] . fix )      //當滿足大根堆時我們就把y根x合併
	{
		fhq [ x ] . rson = merge ( fhq [ x ] . rson , y ) ;      //因為根據二叉搜尋樹性質,y一定在x的右子樹所以就把y根x的左子樹合併
		update ( x ) ;          //因為把y合併到了x上,所以x的長度也會相應得變化,所以說要更新一下
		return x ;            //返回合併後的節點的值
	}
	else 
	{
		fhq [ y ] . lson = merge ( x , fhq [ y ] . lson ) ;      //同上,不過相反
		update( y ) ;            
		return y ;
	}
}

    以上便是FHQ-Treap用來維護樹平衡的方法,是不是跟旋轉不同,但又覺得,好像比旋轉複雜,其實,你看它的格式,基本上都是寫一段,然後複製一段,然後反過來寫(如:x改成y,lson變成rson)。
    那麼我們來講講如何實現基本操作。

基本操作一:插入

    我們可以把樹按照要插入的值分裂開來,y上的值都比這個數大,x則相反,因此按照二叉查詢樹性質,我們可以把這個數根x合併,然後再把合併後的子樹根y合併,此時,就成功把這個數插進去了。
    上程式碼!

inline void Insert ( long long val )            //val表示要插進去的數
{
	spilt ( root , val , x , y ) ;            //分成兩棵子樹
	long long up_new = New_Node ( val ) ;      //申請一個新的節點
	long long up_x = merge ( x , up_new );      //把新節點看做一棵子樹跟x合併
	root = merge ( up_x , y ) ;      //最後把x跟y合併
}

基本操作二:刪除

    關於刪除,其實也很簡單。
    首先把樹按照val分成x,z兩棵樹。此時x是<=val,z是>val-1的。
    然後,我們再把子樹x按val-1分成x和y兩部分,此時x<=val-1,y>val-1的,又因為原本的x子樹上的值全部都是<=val因此這裡的y是val>=y>val-1,所以y=val,所以我們只需要在y上面隨便刪除一個節點就好了,刪除根節點更加方便,因此,我們刪除y子樹上的根節點。
    上程式碼!

inline void Delete ( long long val )            //val表示要刪除的節點的值
{
	spilt ( root , val , x , z ) ;           //把樹按照val拆成x,z兩棵子樹
	spilt ( x , val-1 , x , y ) ;            //把樹按照val-1拆成x,y兩棵子樹
	y = merge ( fhq [ y ] . lson , fhq [ y ] . rson ) ;      //刪除y的根節點,相當於合併它的左右子樹,然後把合併後子樹的根給y,就相當於刪除了原本的y
	long long up_xy = merge ( x , y ) ;                 //拆了就要和,有拆必有合嘛
	root = merge ( up_xy , z ) ; 
}

基本操作三:查詢排名

    這個更水,我們既然是找從小到大排序的,所以我們只需要把以val-1樹分成x,y兩棵子樹,此時x上的樹就全部<=val-1,所以直接用x的size加上1就好了。
    上程式碼!

inline long long get_rank ( long long val )            //val表示要查詢數的排名
{
	spilt ( root , val-1 , x , y ) ;      //按val-1分裂成x,y兩棵子樹
	int retur = fhq [ x ] . size + 1 ;     //直接算出答案
	root = merge ( x , y ) ;      //合併一下,因為分開了
	return retur ;            //返回算出的答案
}

基本操作四:找第k小

    這個稍微有點長,但是好理解,我覺得直接對著程式碼將跟好。(大佬勿懟)
    上程式碼!

inline long long get_num ( long long rank )            //我們需要獲取排名為rank的樹
{
	long long now = root ;            //首先從根節點開始找
	while ( now )
	{
		long long now_rank = fhq [ fhq [ now ] . lson ] . size + 1 ;           //方便後面,先把當前搜尋到的節點的排名存起來
		if ( now_rank == rank ) return fhq [ now ] . val ;      //如果跟要找得排名一樣,那就返回當前節點的值
		if ( now_rank > rank ) now = fhq [ now ] . lson ;       //如果當前排名大於要找的排名,我們就得往左子樹找,因為我們排名是從小到大,且左子樹小於根節點,右子樹大於根節點。
		else      //否則就得往右子樹搜,理由同上,只是反了過來
	        {
			rank -= now_rank ;             //因為已經找過了現在的排名也就是說搜過了那麼多數了,因此我們要減掉,因為我們要找rank所以,相當於我們要在比自己大的右子樹尋找第rank-now_rank。
		      	now = fhq [ now ] . rson ;     //繼續找右子樹 	
		}
	} 
}

基本操作五和六:前驅後繼

    為什麼我要放在一起呢,因為他們兩個也是一樣的,只是相反而已。
    我們先講講前驅,我們可以把樹按照val-1分成x,y兩棵子樹,此時x<=val-1,y>val-1,很顯然,答案在x裡,此時我們要找到最接近的,所以只需要便利x的右子樹就好了。
    後繼同理,我們可以把樹按照val分成x,y兩棵子樹,此時x<=val,y>val,很顯然,答案在y裡,此時我們要找到最接近的,所以只需要便利y的左子樹就好了。
    上程式碼!

inline long long prev ( long long val )      //前驅,val表示我們要找val的前驅
{
	spilt ( root , val-1 , x , y ) ;      //把樹按照val-1分成x,y兩棵子樹
	long long now = x ;                  //答案在x裡因此要便利子樹x
	while( fhq [ now ] . rson && fhq [ fhq [ now ] . rson ] . val != val ) now = fhq [ now ] . rson ;      //一直往x子樹的右兒子便利,直到便利到沒有右孩子為止,此時的now對應的val是滿足條件的最大的。
	root = merge ( x , y ) ;      //有分有合
	return fhq [ now ] . val ;      //返回前驅
}
inline long long succ ( long long val )      //後繼,val表示我們要找val的後繼
{
	spilt ( root , val , x , y ) ;      //同上,只是相反
	long long now = y ;
	while( fhq [ now ] . lson && fhq [ fhq [ now ] . lson ] . val != val ) now = fhq [ now ] . lson ;
	root = merge ( x , y ) ;
	return fhq [ now ] . val ;
}

以上便是關於FHQ-Treap的一些基本知識,當然,FHQ-Treap也可以解決文藝平衡樹的區間問題,這裡只是入門,現在上個模板程式碼:

#include<bits/stdc++.h>
using namespace std;
struct Node
{
	long long val , size , fix ;
	long long fa , lson , rson ;
} fhq [ 100006 ] ;
long long cur ;
long long root ;
inline long long New_Node ( long long val )
{
	long long sd=1e6*2+5;
	cur ++ ;
	fhq [ cur ] . val = val ;
	fhq [ cur ] . fix = rand() % sd ;
	fhq [ cur ] . size = 1 ;
	return cur ;
}
inline void update ( long long now )
{
	fhq [ now ] . size = 0;
	fhq [ now ] . size += fhq [ fhq [ now ] . lson ] . size ;
	fhq [ now ] . size += fhq [ fhq [ now ] . rson ] . size ;
	fhq [ now ] . size ++ ;
}
long long x , y , z ;
inline void spilt ( long long now , long long val , long long &x , long long &y )
{
	if( !now ) x = y = 0 ;
	else 
	{
		if(fhq [ now ] . val <= val)
		{
			x = now ;
			spilt(fhq [ now ] . rson , val , fhq [ now ] . rson , y ) ;
		}
		else
		{
			y = now ;
			spilt(fhq [ now ] . lson , val , x , fhq [ now ] . lson ) ;
		}
		update(now);
	}
}
inline long long merge ( long long &x , long long &y )
{
	if( !x || !y )return x + y ;
	if( fhq [ x ] . fix > fhq [ y ] . fix )
	{
		fhq [ x ] . rson = merge ( fhq [ x ] . rson , y ) ;
		update ( x ) ;
		return x ;
	}
	else 
	{
		fhq [ y ] . lson = merge ( x , fhq [ y ] . lson ) ;
		update( y ) ;
		return y ;
	}
}
inline void Insert ( long long val )
{
	spilt ( root , val , x , y ) ;
	long long up_new = New_Node ( val ) ;
	long long up_x = merge ( x , up_new );
	root = merge ( up_x , y ) ;
}
inline void Delete ( long long val )
{
	spilt ( root , val , x , z ) ;
	spilt ( x , val-1 , x , y ) ;
	y = merge ( fhq [ y ] . lson , fhq [ y ] . rson ) ;
	long long up_xy = merge ( x , y ) ;
	root = merge ( up_xy , z ) ; 
}
inline long long prev ( long long val )
{
	spilt ( root , val-1 , x , y ) ;
	long long now = x ;
	while( fhq [ now ] . rson && fhq [ fhq [ now ] . rson ] . val != val ) now = fhq [ now ] . rson ;
	root = merge ( x , y ) ;
	return fhq [ now ] . val ;
}
inline long long succ ( long long val )
{
	spilt ( root , val , x , y ) ;
	long long now = y ;
	while( fhq [ now ] . lson && fhq [ fhq [ now ] . lson ] . val != val ) now = fhq [ now ] . lson ;
	root = merge ( x , y ) ;
	return fhq [ now ] . val ;
}
inline long long get_rank ( long long val )
{
	spilt ( root , val-1 , x , y ) ;
	int retur = fhq [ x ] . size + 1 ;
	root = merge ( x , y ) ;
	return retur ;// x<val-1  y>val-1 從小到大
}
inline long long get_num ( long long rank )
{
	long long now = root ;
	while ( now )
	{
		long long now_rank = fhq [ fhq [ now ] . lson ] . size + 1 ;
		if ( now_rank == rank ) return fhq [ now ] . val ;
		if ( now_rank > rank ) now = fhq [ now ] . lson ; 
		else
	    {
			rank -= now_rank ; 
			now = fhq [ now ] . rson ; 	
		}
	} 
	return 0 ;
}
long long n ;
int main()
{
//	freopen( "P3369_6.in" , "r" , stdin ) ;
//	freopen( "1.txt" , "w" , stdout ) ;
	srand ( ( unsigned ) time ( NULL ) ) ;
	scanf ( "%lld" , & n ) ;
	for ( long long i = 1 ; i <= n ; i ++ )
	{
		long long o , b ;
		scanf ( "%lld%lld" , & o , & b );
		if ( o == 1 ) Insert ( b ) ;
		if ( o == 2 ) Delete ( b ) ;
		if ( o == 3 ) printf ( "%lld\n" , get_rank ( b ) ) ;
		if ( o == 4 ) printf ( "%lld\n" , get_num ( b ) ) ;
		if ( o == 5 ) printf ( "%lld\n" , prev ( b ) ) ;
		if ( o == 6 ) printf ( "%lld\n" , succ ( b ) ) ;
	}
	return 0 ;
}
     推薦幾道題目:
      洛谷 P3369 【模板】普通平衡樹
      洛谷 P6136 【模板】普通平衡樹(資料加強版)
      洛谷 P1503 鬼子進村

感謝您的閱讀,有什麼寫得不好的請大佬指出,謝謝大佬%%%