Treap
Treap=Tree+Heap,即在普通二叉查找樹的基礎上每個節點有了一個新值域:強化值(因為它將普通二叉查找樹強化為treap就自己起了這個名字,是用來滿足堆性質的,即后文說滿足堆性質都指強化值滿足堆性質)。要求這個樹節點的鍵值(即要代表的數)滿足BST的性質、強化值滿足小跟堆的性質(你非得大根堆也沒什么)。強化值由建立節點時由一個隨機算法(rand())給出,在一個以隨機數據建成的堆的加持下,treap的期望高度被證明為logn,故是一個平衡樹。
代碼請看文末
核心操作:旋轉
左旋(zig):左旋一棵子樹,它的根變為新子樹的左兒子、根的右兒子變為新子樹的根,那么根的右兒子的新左兒子是根了,原來的左兒子怎么辦?交給根剛好。這樣操作會發現,BST的性質仍然滿足(相對左右位置未變),整個樹宏觀上向左“轉動”了一下。
右旋(zig):右旋一棵子樹,它的根變為新子樹的右兒子、根的左兒子變為新子樹的根,根的右兒子的原左兒子也可交給根。這樣操作會發現,BST的性質仍然滿足(相對左右位置未變),整個樹宏觀上向右“轉動”了一下。
故:旋轉不改變BST性質,但會改變父祖關系。同時不改變如圖x、y、z三部分的堆性質(不特指某種旋轉。紅點為即將認父的根,綠點為即將認兒的子節點。x為會變為綠點的外側子樹,z代表原根的另一個子樹,y代表內側綠點會給紅點的子樹)
泛用操作:
1、插入x數:
按照普通二叉查找樹插入方式新建節點后使其獲得強化值。回溯過程中看兒子:左兒子強化值小于自己——右旋(讓他當新爹,小根堆嘛);右兒子強化值小于自己——左旋。
解答為何用旋轉方式維護堆性質:首先旋轉不改變BST性質,但可以改變父子關系。看上圖,若綠點強化值小于紅點,有堆性質得,綠點要當紅點父親才行。一開始綠點子樹一定是滿足堆性質的(只有它一個點),因為綠點強化值小于紅點,所以綠點完全可以當紅點子樹的根。由于紅點子樹在插入值前滿足堆性質,而綠點一定是旋轉上來的,所以紅點可以當除綠點外子樹所有點的父親/祖先,故旋轉完成后,紅點為首的子樹變為綠點為首的子樹,同時整個子樹都滿足堆性質了。
2、刪除x數:
思路類似普通二叉查找樹,找到節點后,若cnt(為了考慮重復值而設的)>1,則cnt--,否則,若:
其最多只有一個子節點:讓子節點代替他(若有的話),若沒有,直接刪就好。
有兩個子節點:旋轉,讓強化值小的子節點當新爹,把原爹(即要刪的)旋下去,直到刪它的情況變為第一種。
解答這里為何旋轉:強化值小的子節點可以當原子樹內除原爹外所有點的父親/祖先,旋轉后子樹不含原爹為首新子樹的部分仍滿足堆性質。以原爹為首的新子樹除了原爹,強化值最小的(即原爹的原另一個強化值較大的子節點)點也沒原爹現在的新爹(即原爹的原強化值較小的子節點)小,故可預知刪除原爹后,整個樹仍滿足堆性質。
3、查詢x數的排名 4、查詢排名為x的數 5、求x的前驅 6、求x的后繼 這些操作與二叉查找樹的操作無異,見:二叉查找樹
7、分離:
要把一個Treap按大小分成兩個Treap,即大于等于x的分離成一個treap,剩下的也成一個treap,只要加一個虛擬節點(在需要分開的兩點間,或某個葉子結點的兒子,看實際情況。怎么找?前驅或后繼),然后將虛擬節點旋至根節點刪除,左右兩個子樹就是得出的兩個Treap了。根據二叉排序樹的性質,這時左子樹的所有節點都小于右子樹的節點。時間相當于一次插入操作的復雜度,也就是 log( n )
8、合并:
合并是指把兩個Treap合并成一個Treap,本文指其中第一個Treap的所有節點都必須小于或等于第二個Treap中的所有節點,先不涉及兩個普通treap的合并。只要加一個虛擬的根,把兩棵樹分別作為左右子樹,然后把根刪除就可以了。時間復雜度和刪除一樣,也是期望O(log n)
練習題:
洛谷P3369 【模板】普通平衡樹
#include<iostream>
#include<cstdio>
#include<algorithm>
//#include<cstdlib>
using namespace std;
const int N=100005;
int n,root,bnt;
struct node{
int ls,rs,cnt,siz,val,dev;
}tre[N];
char ch;
bool f;
inline int read()
{
int x=0;
f=0;
ch=getchar();
while(!isdigit(ch))
f|=ch=='-',ch=getchar();
while(isdigit(ch))
x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
return f?-x:x;
}
inline void upt(int u)
{
tre[u].siz=tre[tre[u].ls].siz+tre[tre[u].rs].siz+tre[u].cnt;
}
inline void zig(int &u)
{
int v=tre[u].rs;
tre[u].rs=tre[v].ls;
tre[v].siz=tre[u].siz;
tre[v].ls=u;
upt(u);
u=v;
}
inline void zag(int &u)
{
int v=tre[u].ls;
tre[u].ls=tre[v].rs;
tre[v].siz=tre[u].siz;
tre[v].rs=u;
upt(u);
u=v;
}
void insert(int &u,const int &val)
{
if(!u)
{
u=++bnt;
tre[u].val=val;
tre[u].cnt=tre[u].siz=1;
tre[u].dev=rand();
return;
}
tre[u].siz++;
if(tre[u].val==val)
{
tre[u].cnt++;
return;
}
if(val<tre[u].val)
{
insert(tre[u].ls,val);
if(tre[tre[u].ls].dev<tre[u].dev)
zag(u);
}
else
{
insert(tre[u].rs,val);
if(tre[tre[u].rs].dev<tre[u].dev)
zig(u);
}
}
void del(int &u,const int &val)
{
if(!u)
return;
if(tre[u].val==val)
{
if(tre[u].cnt>1)
{
tre[u].cnt--;
tre[u].siz--;
}
else
{
if(tre[u].ls&&tre[u].rs)
{
if(tre[tre[u].ls].dev<=tre[tre[u].rs].dev)
{
zag(u);
tre[u].siz--;
del(tre[u].rs,val);
}
else
{
zig(u);
tre[u].siz--;
del(tre[u].ls,val);
}
}
else
if(tre[u].ls||tre[u].rs)
u=tre[u].ls+tre[u].rs;
else
u=0;
}
return;
}
tre[u].siz--;
if(val<tre[u].val)
del(tre[u].ls,val);
else
del(tre[u].rs,val);
}
int finrank(const int &u,const int &val)
{
if(!u)
return 1;
if(tre[u].val==val)
return tre[tre[u].ls].siz+1;
if(val<tre[u].val)
return finrank(tre[u].ls,val);
else
return finrank(tre[u].rs,val)+tre[tre[u].ls].siz+tre[u].cnt;
}
int finval(const int &u,const int &rnk)
{
if(rnk<=tre[tre[u].ls].siz)
return finval(tre[u].ls,rnk);
if(rnk>tre[tre[u].ls].siz+tre[u].cnt)
return finval(tre[u].rs,rnk-tre[tre[u].ls].siz-tre[u].cnt);
return tre[u].val;
}
int finpre(const int &u,const int &val)
{
if(!u)
return -99999999;
if(tre[u].val<val)
{
int k=finpre(tre[u].rs,val);
return max(k,tre[u].val);
}
else
return finpre(tre[u].ls,val);
}
int finnxt(const int &u,const int &val)
{
if(!u)
return 99999999;
if(tre[u].val>val)
{
int k=finnxt(tre[u].ls,val);
return min(k,tre[u].val);
}
else
return finnxt(tre[u].rs,val);
}
int main()
{
srand(9999);
int n=read();
int opt,x;
while(n--)
{
opt=read(),x=read();
switch(opt)
{
case 1:
insert(root,x);
break;
case 2:
del(root,x);
break;
case 3:
printf("%d
",finrank(root,x));
break;
case 4:
printf("%d
",finval(root,x));
break;
case 5:
printf("%d
",finpre(root,x));
break;
case 6:
printf("%d
",finnxt(root,x));
break;
}
}
return 0;
}
既是示例代碼也是題解
結語:維持Treap的平衡性,強化值有著決定性的作用,故有時臉黑TLE也不是沒有可能的。。。
補充:普通treap是不能O(log n)做區間操作的。為什么?因為你對al~ar沒法快速標記。如確實想用treap做區間操作,移步fhq treap。因為fhq treap可將一個區間內的點全都裂成一個樹,給這個樹根打標記就完成了對整個區間的標記。
EX:洛谷日報:不用旋轉的treap?——fhq treapbug賊多不推薦了
參考資料:
Treap百度百科
總結
- 上一篇: 平坝酒怎么样 品鉴平坝酒的口感和特色?
- 下一篇: 面条家常做法大全?