Treap 学习笔记
二叉查找樹
二叉查找樹是一棵有點權(quán)的二叉樹,具有以下幾個特征:
- 左孩子的權(quán)值小于父親的權(quán)值
- 右孩子的權(quán)值大于父親的權(quán)值
- 中序遍歷及從小到大排序
二叉查找樹支持以下幾個操作:
- 插入一個數(shù)
- 刪除一個數(shù)
- 找一個數(shù)的前驅(qū)
- 找一個數(shù)的后繼
- 詢問一個數(shù)的排名
- 詢問排第幾名的數(shù)
二叉查找樹一棵二叉查找樹,所以在最優(yōu)的情況下單一操作的時間復(fù)雜度應(yīng)該是 \(\text{O}(\log n)\) 級別的。但是在進(jìn)行操作時,如果輸入的點權(quán)單調(diào)遞增或遞減,那么整個數(shù)據(jù)結(jié)構(gòu)就將由樹退化成為鏈。所以單次操作的時間復(fù)雜度最壞為 \(\text{O}(n)\) 級別。
普通平衡樹
為了使這個數(shù)據(jù)結(jié)構(gòu)平衡,平衡樹就應(yīng)運而生了。Treap 就是平衡樹的一種,這個算法就是將樹 (Tree) 與堆 (Heap) 相結(jié)合了起來。Treap 給每一個節(jié)點在維護(hù)原來的數(shù)值的同時,還添加了一個隨機值。但看權(quán)值,這是一顆二叉搜索樹,但是但看隨機值這又是一個堆。
儲存
首先我們應(yīng)該了解一下如何儲存一顆平衡樹。
因為平衡樹的結(jié)構(gòu)是會改變的,所以我們需要儲存每一個節(jié)點的左孩子與右孩子。因為一個節(jié)點可能會多次添加,所以應(yīng)該使用 cnt 記錄以下這個節(jié)點出現(xiàn)的個數(shù)。為了后面的操作,我們應(yīng)該還需要定義一個 size 變量記錄這個節(jié)點及子樹的大小。
所以在我們定義的結(jié)構(gòu)體應(yīng)該是下面這樣的:
struct node{
int l,r,k,val,cnt,size;
}a[N];
updata
在進(jìn)行修改操作之后,節(jié)點的子樹大小會發(fā)行變化。updata 函數(shù)的功能是更新節(jié)點的 size 值。
void updata(int u){
a[u].size=a[a[u].l].size+a[a[u].r].size+a[u].cnt;
}
make
在進(jìn)行操作時,為了節(jié)省空間復(fù)雜度,平衡樹使用了動態(tài)開點。動態(tài)開點就是你需要使用一個新節(jié)點時就現(xiàn)馬上申請一個空間,而不是全部預(yù)留好。
int make(int k){
a[++tot].k=k,a[tot].val=rand(); //tot 記錄節(jié)點個數(shù)
a[tot].cnt=a[tot].size=1;
return tot;
}
zig && zag
既然需要再維護(hù)二叉查找樹的同時維護(hù)平衡樹,就需要在不改變平衡樹的性質(zhì)的情況下完成堆所需要的 swap 的操作。所以我們就迎來了平衡樹最重要的操作 zig 與 zag。
這是一棵平衡樹,其中 1 2 3 為節(jié)點 A B C 為子樹。
它們滿足以下性質(zhì):\(1>A>2>C>3>D\)
那么如果需要交換 2 3 的位置,那么在不違背其性質(zhì)的情況下將其改為:
這個過程就是 zig 操作,反之即是 zag 操作。代碼實現(xiàn)就是將將操作進(jìn)行模擬,方法如下:
void zig(int &p){
int q=a[p].l;
a[p].l=a[q].r,a[q].r=p,p=q;
updata(a[p].r),updata(p);
}
void zag(int &p){
int q=a[p].r;
a[p].r=a[q].l,a[q].l=p,p=q;
updata(a[p].l),updata(p);
}
build
因為在平衡樹中有旋轉(zhuǎn)操作,所以根節(jié)點有可能會在旋轉(zhuǎn)操作中改變位置。為了讓根節(jié)點的位置保持不變,可以建立兩個虛點,并令其優(yōu)先級遠(yuǎn)遠(yuǎn)高于其他的點,永遠(yuǎn)停留在根節(jié)點的位置。
void build(){
make(-INF),make(INF);
root=1,a[1].r=2,updata(root);
if(a[1].val<a[2].val) zag(root);
}
insert
在插入操作中,一共有三種操作。反復(fù)執(zhí)行操作三,直至滿足操作一或操作二。
-
操作一:需要處理的節(jié)點為 \(0\),意味著這個節(jié)點不存在,所以直接新建。
-
操作二:已經(jīng)找到車要添加的節(jié)點,
cnt加一。 -
操作三:需要添加的節(jié)點小于或大于這個節(jié)點,那么分別訪問左節(jié)點或右節(jié)點。
void insert(int &p,int k){
if(p==0) p=make(k);
else{
if(a[p].k==k) a[p].cnt++;
if(a[p].k>k){
insert(a[p].l,k);
if(a[a[p].l].val>a[p].val) zig(p);
}if(a[p].k<k){
insert(a[p].r,k);
if(a[a[p].r].val>a[p].val) zag(p);
}
}updata(p);
}
del
在刪除操作中,同樣分為三種操作:
-
操作一:沒有找到這個點就直接返回,不進(jìn)行修改操作。
-
操作二:如果這個節(jié)點的值大于或者小于要刪除的值,那么就繼續(xù)訪問左孩子或者右孩子。
-
操作三:找到了這個值,如果
cnt大于 \(1\),那么直接cnt--否則尋找比這個節(jié)點大的集合中的最小值。
void del(int &p,int k){
if(p==0) return ;
if(a[p].k==k){
if(a[p].cnt>1){
a[p].cnt--;
updata(p);
return;
}if(a[p].l||a[p].r){
if(!a[p].r||a[a[p].l].val) zig(p),del(a[p].r,k);
else zag(p),del(a[p].l,k);
}else p=0;
updata(p);
return;
}if(a[p].k>k) del(a[p].l,k);
else del(a[p].r,k);
updata(p);
}
get_rank
get_rank 函數(shù)可以獲得某個點的排名。在尋找時如果節(jié)點在左子樹,則這個節(jié)點在左子樹的排名就是這個節(jié)點在這棵子樹上的排名。反之,如果這個節(jié)點在右子樹,那么他的排名就是左子樹的大小+根節(jié)點的大小+自己在右子樹的排名。
int get_rank(int p,int k){
if(p==0) return 0;
if(a[p].k==k) return a[a[p].l].size+1;
if(a[p].k>k) return get_rank(a[p].l,k);
return a[a[p].l].size+a[p].cnt+get_rank(a[p].r,k);
}
因為查詢的數(shù)可能不在樹中存在,所以但是 get_rank 的返回值又是默認(rèn)其存在的,所以將答案設(shè)為了函數(shù)值\(-1\)。為了避免發(fā)生這樣的錯誤,需要在定義一個 find 函數(shù)檢查是否存在這個節(jié)點。
bool find(int p,int x){
if(a[p].k==x) return 0;
if(a[p].val==0) return 1;
if(a[p].k>x) return find(a[p].l,x);
return find(a[p].r,x);
}
get_key
get_key 函數(shù)可以獲取某個排名的數(shù)。當(dāng)訪問到一個節(jié)點時,如果這個節(jié)點的左子樹的大小大于它的排名,那么這個節(jié)點就應(yīng)該在左子樹。如果這個排名大于這個節(jié)點的大小 + 左子樹的大小,那么這個節(jié)點就應(yīng)該在右子樹。其他的情況就應(yīng)該就在這個節(jié)點。
int get_key(int p,int rank){
if(p==0) return INF;
if(a[a[p].l].size>=rank) return get_key(a[p].l,rank);
if(a[a[p].l].size+a[p].cnt>=rank) return a[p].k;
return get_key(a[p].r,rank-a[a[p].l].size-a[p].cnt);
}
get_pr
get_pr 函數(shù)可以找到一個數(shù)的前驅(qū),及比他大的數(shù)中最小的一個。因為平衡樹滿足左孩子 \(<\) 根節(jié)點 \(<\) 右孩子,所以只需要先走到左孩子,再一直向右走就可以了。
int get_pr(int p,int k){
if(p==0) return-INF;
if(a[p].k>=k) return get_pr(a[p].l,k);
return max(get_pr(a[p].r,k),a[p].k);
}
get_ne
get_ne 函數(shù)可以找到一個數(shù)的后驅(qū),及比他小的數(shù)中最大的一個。因為平衡樹滿足左孩子 \(<\) 根節(jié)點 \(<\) 右孩子,所以只需要先走到右孩子,再一直向左走就可以了。
int get_ne(int p,int k){
if(p==0) return INF;
if(a[p].k<=k) return get_ne(a[p].r,k);
return min(get_ne(a[p].l,k),a[p].k);
}
P3369 普通平衡樹
這一題就是一道模板題,只需要將前面的操作整合在一起就可以了。
#include<bits/stdc++.h>
using namespace std;
const int N=100010,INF=1e8;
int n;
struct Node{int l,r,k,val,cnt,size;}a[N];
int root,tot;
void updata(int u){a[u].size=a[a[u].l].size+a[a[u].r].size+a[u].cnt;}
int make(int k){
a[++tot].k=k,a[tot].val=rand();
a[tot].cnt=a[tot].size=1;
return tot;
}
void zig(int &p){
int q=a[p].l;
a[p].l=a[q].r,a[q].r=p,p=q;
updata(a[p].r),updata(p);
}
void zag(int &p){
int q=a[p].r;
a[p].r=a[q].l,a[q].l=p,p=q;
updata(a[p].l),updata(p);
}
void build(){
make(-INF),make(INF);
root=1,a[1].r=2,updata(root);
if(a[1].val<a[2].val) zag(root);
}
void insert(int &p,int k){
if(p==0) p=make(k);
else{
if(a[p].k==k) a[p].cnt++;
if(a[p].k>k){
insert(a[p].l,k);
if(a[a[p].l].val>a[p].val) zig(p);
}if(a[p].k<k){
insert(a[p].r,k);
if(a[a[p].r].val>a[p].val) zag(p);
}
}updata(p);
}
void del(int &p,int k){
if(p==0) return ;
if(a[p].k==k){
if(a[p].cnt>1){
a[p].cnt--;
updata(p);
return;
}if(a[p].l||a[p].r){
if(!a[p].r||a[a[p].l].val) zig(p),del(a[p].r,k);
else zag(p),del(a[p].l,k);
}else p=0;
updata(p);
return;
}if(a[p].k>k) del(a[p].l,k);
else del(a[p].r,k);
updata(p);
}
int get_rank(int p,int k){
if(p==0) return 0;
if(a[p].k==k) return a[a[p].l].size+1;
if(a[p].k>k) return get_rank(a[p].l,k);
return a[a[p].l].size+a[p].cnt+get_rank(a[p].r,k);
}
int get_key(int p,int rank){
if(p==0) return INF;
if(a[a[p].l].size>=rank) return get_key(a[p].l,rank);
if(a[a[p].l].size+a[p].cnt>=rank) return a[p].k;
return get_key(a[p].r,rank-a[a[p].l].size-a[p].cnt);
}
int get_pr(int p,int k){
if(p==0) return-INF;
if(a[p].k>=k) return get_pr(a[p].l,k);
return max(get_pr(a[p].r,k),a[p].k);
}
int get_ne(int p,int k){
if(p==0) return INF;
if(a[p].k<=k) return get_ne(a[p].r,k);
return min(get_ne(a[p].l,k),a[p].k);
}
bool find(int p,int x){
if(a[p].k==x) return 0;
if(a[p].val==0) return 1;
if(a[p].k>x) return find(a[p].l,x);
return find(a[p].r,x);
}
int main(){
build();
cin>>n;
for(int i=1,op,x;i<=n;i++){
cin>>op>>x;
if(op==1) insert(root,x);
if(op==2) del(root,x);
if(op==3) cout<<get_rank(root,x)+find(root,x)-1;
if(op==4) cout<<get_key(root,x+1);
if(op==5) cout<<get_pr(root,x);
if(op==6) cout<<get_ne(root,x);
if(op!=1&&op!=2)cout<<endl;
}return 0;
}
總結(jié)
以上是生活随笔為你收集整理的Treap 学习笔记的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php无法加载动态库怎么办
- 下一篇: AR9271无线网卡Win10配置热点