Treap
Treap=Tree+Heap,即在普通二叉查找樹的基礎(chǔ)上每個節(jié)點(diǎn)有了一個新值域:強(qiáng)化值(因?yàn)樗鼘⑵胀ǘ娌檎覙鋸?qiáng)化為treap就自己起了這個名字,是用來滿足堆性質(zhì)的,即后文說滿足堆性質(zhì)都指強(qiáng)化值滿足堆性質(zhì))。要求這個樹節(jié)點(diǎn)的鍵值(即要代表的數(shù))滿足BST的性質(zhì)、強(qiáng)化值滿足小跟堆的性質(zhì)(你非得大根堆也沒什么)。強(qiáng)化值由建立節(jié)點(diǎn)時由一個隨機(jī)算法(rand())給出,在一個以隨機(jī)數(shù)據(jù)建成的堆的加持下,treap的期望高度被證明為logn,故是一個平衡樹。
代碼請看文末
核心操作:旋轉(zhuǎn)
左旋(zig):左旋一棵子樹,它的根變?yōu)樾伦訕涞淖髢鹤印⒏挠覂鹤幼優(yōu)樾伦訕涞母敲锤挠覂鹤拥男伦髢鹤邮歉耍瓉淼淖髢鹤釉趺崔k?交給根剛好。這樣操作會發(fā)現(xiàn),BST的性質(zhì)仍然滿足(相對左右位置未變),整個樹宏觀上向左“轉(zhuǎn)動”了一下。
右旋(zig):右旋一棵子樹,它的根變?yōu)樾伦訕涞挠覂鹤印⒏淖髢鹤幼優(yōu)樾伦訕涞母挠覂鹤拥脑髢鹤右部山唤o根。這樣操作會發(fā)現(xiàn),BST的性質(zhì)仍然滿足(相對左右位置未變),整個樹宏觀上向右“轉(zhuǎn)動”了一下。
故:旋轉(zhuǎn)不改變BST性質(zhì),但會改變父祖關(guān)系。同時不改變?nèi)鐖Dx、y、z三部分的堆性質(zhì)(不特指某種旋轉(zhuǎn)。紅點(diǎn)為即將認(rèn)父的根,綠點(diǎn)為即將認(rèn)兒的子節(jié)點(diǎn)。x為會變?yōu)榫G點(diǎn)的外側(cè)子樹,z代表原根的另一個子樹,y代表內(nèi)側(cè)綠點(diǎn)會給紅點(diǎn)的子樹)
泛用操作:
1、插入x數(shù):
按照普通二叉查找樹插入方式新建節(jié)點(diǎn)后使其獲得強(qiáng)化值。回溯過程中看兒子:左兒子強(qiáng)化值小于自己——右旋(讓他當(dāng)新爹,小根堆嘛);右兒子強(qiáng)化值小于自己——左旋。
解答為何用旋轉(zhuǎn)方式維護(hù)堆性質(zhì):首先旋轉(zhuǎn)不改變BST性質(zhì),但可以改變父子關(guān)系。看上圖,若綠點(diǎn)強(qiáng)化值小于紅點(diǎn),有堆性質(zhì)得,綠點(diǎn)要當(dāng)紅點(diǎn)父親才行。一開始綠點(diǎn)子樹一定是滿足堆性質(zhì)的(只有它一個點(diǎn)),因?yàn)榫G點(diǎn)強(qiáng)化值小于紅點(diǎn),所以綠點(diǎn)完全可以當(dāng)紅點(diǎn)子樹的根。由于紅點(diǎn)子樹在插入值前滿足堆性質(zhì),而綠點(diǎn)一定是旋轉(zhuǎn)上來的,所以紅點(diǎn)可以當(dāng)除綠點(diǎn)外子樹所有點(diǎn)的父親/祖先,故旋轉(zhuǎn)完成后,紅點(diǎn)為首的子樹變?yōu)榫G點(diǎn)為首的子樹,同時整個子樹都滿足堆性質(zhì)了。
2、刪除x數(shù):
思路類似普通二叉查找樹,找到節(jié)點(diǎn)后,若cnt(為了考慮重復(fù)值而設(shè)的)>1,則cnt--,否則,若:
其最多只有一個子節(jié)點(diǎn):讓子節(jié)點(diǎn)代替他(若有的話),若沒有,直接刪就好。
有兩個子節(jié)點(diǎn):旋轉(zhuǎn),讓強(qiáng)化值小的子節(jié)點(diǎn)當(dāng)新爹,把原爹(即要刪的)旋下去,直到刪它的情況變?yōu)榈谝环N。
解答這里為何旋轉(zhuǎn):強(qiáng)化值小的子節(jié)點(diǎn)可以當(dāng)原子樹內(nèi)除原爹外所有點(diǎn)的父親/祖先,旋轉(zhuǎn)后子樹不含原爹為首新子樹的部分仍滿足堆性質(zhì)。以原爹為首的新子樹除了原爹,強(qiáng)化值最小的(即原爹的原另一個強(qiáng)化值較大的子節(jié)點(diǎn))點(diǎn)也沒原爹現(xiàn)在的新爹(即原爹的原強(qiáng)化值較小的子節(jié)點(diǎn))小,故可預(yù)知刪除原爹后,整個樹仍滿足堆性質(zhì)。
3、查詢x數(shù)的排名 4、查詢排名為x的數(shù) 5、求x的前驅(qū) 6、求x的后繼 這些操作與二叉查找樹的操作無異,見:二叉查找樹
7、分離:
要把一個Treap按大小分成兩個Treap,即大于等于x的分離成一個treap,剩下的也成一個treap,只要加一個虛擬節(jié)點(diǎn)(在需要分開的兩點(diǎn)間,或某個葉子結(jié)點(diǎn)的兒子,看實(shí)際情況。怎么找?前驅(qū)或后繼),然后將虛擬節(jié)點(diǎn)旋至根節(jié)點(diǎn)刪除,左右兩個子樹就是得出的兩個Treap了。根據(jù)二叉排序樹的性質(zhì),這時左子樹的所有節(jié)點(diǎn)都小于右子樹的節(jié)點(diǎn)。時間相當(dāng)于一次插入操作的復(fù)雜度,也就是 log( n )
8、合并:
合并是指把兩個Treap合并成一個Treap,本文指其中第一個Treap的所有節(jié)點(diǎn)都必須小于或等于第二個Treap中的所有節(jié)點(diǎn),先不涉及兩個普通treap的合并。只要加一個虛擬的根,把兩棵樹分別作為左右子樹,然后把根刪除就可以了。時間復(fù)雜度和刪除一樣,也是期望O(log n)
練習(xí)題:
洛谷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;
}
既是示例代碼也是題解
結(jié)語:維持Treap的平衡性,強(qiáng)化值有著決定性的作用,故有時臉黑TLE也不是沒有可能的。。。
補(bǔ)充:普通treap是不能O(log n)做區(qū)間操作的。為什么?因?yàn)槟銓l~ar沒法快速標(biāo)記。如確實(shí)想用treap做區(qū)間操作,移步fhq treap。因?yàn)閒hq treap可將一個區(qū)間內(nèi)的點(diǎn)全都裂成一個樹,給這個樹根打標(biāo)記就完成了對整個區(qū)間的標(biāo)記。
EX:洛谷日報:不用旋轉(zhuǎn)的treap?——fhq treapbug賊多不推薦了
參考資料:
Treap百度百科
總結(jié)
- 上一篇: 平坝酒怎么样 品鉴平坝酒的口感和特色?
- 下一篇: 绩点怎么算(5分制的绩点对照表)