前置技能
二叉搜索樹
堆
前置技能有旋轉treap?不存在的(我到現在仍然不會旋轉的treap qwq)
Treap簡介
引用維基百科上一句精闢的話:Treap=Tree+Heap
在Treap上需要維護兩個值:一個優先級,一個節點權值。
其中優先級取隨機數,滿足小根堆的性質。節點權值滿足二叉搜索樹的性質。
即每個節點的值均小於左、右孩子的值,每個節點的左子樹的均小於這個節點的值,右子樹的大於這個節點的值。
示例:
顯然,Treap的每棵子樹都是一棵Treap。
珂以證明(雖然我不會證qwq),Treap的樹高爲。
普通的Treap是通過旋轉來維護其性質的,而FHQ Treap通過合併(Merge)和分裂(Split)來維護qwq。
Merge操作
比如要合併兩棵分別以爲根的樹,假設的值<=的值。
然後按優先級決定誰是根qwq,當的值比的值小時,爲新的根,否則爲新的根,這樣珂以使仍然滿足小根堆的性質。
當的值更小時,的值小於的值,根據二叉搜索樹的性質,應該把放到右子樹中。因此把的右孩子與合併。
當的值更小時,的值小於的值,所以應該把放到左子樹中,因此把的左孩子與合併。
最後需要更新根節點的值qwq(即子樹內的節點個數)
Merge 的毒瘤代碼:
inline void Update(int x) { //更新子樹大小
tree[x].siz=tree[lc(x)].siz+tree[rc(x)].siz+1;
}
int Merge(int x,int y) {
//合併以x和y爲根的樹
if(!x || !y) return x+y;
//用優先級決定新的根是x還是y
if(tree[x].pri<tree[y].pri) {
rc(x)=Merge(rc(x),y);
Update(x);
return x;
} else {
lc(y)=Merge(x,lc(y));
Update(y);
return y;
}
}
Split操作
這裏講述的是把一棵樹按權值分爲兩棵子樹的方法。
假設要把的子樹中權值的分到以爲根的Treap中,剩下的權值的分到以爲根的Treap中。
當的權值時,根據二叉搜索樹的性質,的左子樹中的所有節點權值均。
所以把和的左子樹內的所有節點都分給,然後遞歸把的右子樹中的分到的右子樹(這樣仍然滿足爲二叉搜索樹的性質),的右子樹中的分給即可。
當的權值時,的右子樹中的所有節點權值均。
同理,把和右子樹內的所有節點均分給,然後遞歸把的左子樹中的的分到的左子樹,的左子樹中的分給即可qwq。
Split 的毒瘤代碼:
void Split(int i,int k,int &x,int &y) {
//把以i爲根的樹分成以x爲根和以y爲根的樹
//把所有權值<=k的分到x中,>k的分到y中
if(!i) {
//若i爲空節點,則x和y也爲空
x=y=0;
} else {
if(tree[i].val<=k) {
//這裏先讓x=i,然後遞歸下去就把i的右子樹中<=k的分到了rc(x)
x=i;
Split(rc(i),k,rc(x),y);
} else {
//同理
y=i;
Split(lc(i),k,x,lc(y));
}
Update(i); //記得更新子樹大小
}
}
找權值第k大
因爲Treap的滿足二叉搜索樹性質,所以類似地按照二叉搜索樹的方法找即可。
比較巧妙的一點是:表示左子樹加上根這個節點的總節點數qwq。
代碼:
int kth(int i,int k) {
//找到i的子樹中val第k大的點的下標
while(true) {
if(k<=tree[lc(i)].siz) {
//若k比左子樹大小還小
//則第k大顯然在左子樹中
i=lc(i);
} else if(k>tree[lc(i)].siz+1) {
//同理
k-=tree[lc(i)].siz+1;
i=rc(i);
} else {
return i;
}
}
}
另外放一個新建節點的New函數代碼:
inline int New(int v) { //新建節點並返回其下標
tree[++tot].val=v;
tree[tot].pri=rand();
tree[tot].siz=1;
return tot;
}
例題
洛谷P3369 【模板】普通平衡樹
即 LOJ #104 普通平衡樹
您需要寫一種數據結構(可參考題目標題),來維護一些數,其中需要提供以下操作:
1.插入數
2.刪除數(若有多個相同的數,因只刪除一個)
3.查詢數的排名(排名定義爲比當前數小的數的個數+1。若有多個相同的數,因輸出最小的排名)
4.查詢排名爲的數
5.求的前驅(前驅定義爲小於,且最大的數)
6.求的後繼(後繼定義爲大於,且最小的數)
這裏講一下操作1和操作2,其他操作比較簡單,看代碼註釋就珂以了qwq(我不會告訴你只是我懶得寫qwq)
操作1:
因爲Merge操作是需要的權值的權值,所以不能直接New一個節點然後直接大莉Merge qwq。
假設當前需要插入的節點權值爲num,我們考慮把這棵Treap按照權值分爲兩棵樹,權值的分到,的分到。
所以珂以把和New得到的節點合併,再把得到的結果與合併。
操作2:
假設要刪掉一個權值爲的節點。
這裏珂以考慮把所有權值爲的節點分到一棵樹中,然後刪根節點。
具體實現方法:把所有權值的節點分到,權值的分到。
然後把權值的分到,權值的分到。
不難發現這樣中存的都是權值爲的點,此時把根節點刪掉即可qwq
例題代碼
#include<stdio.h>
#include<cstring>
#include<algorithm>
#include<vector>
#define re register int
#define lc(x) tree[x].ls
#define rc(x) tree[x].rs
using namespace std;
typedef long long ll;
int read() {
re x=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9') {
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9') {
x=10*x+ch-'0';
ch=getchar();
}
return x*f;
}
const int Size=200005;
struct node {
int ls,rs; //左右孩子
int val; //權值,左<根<右
int pri; //優先級,維護小根堆
int siz; //子樹大小
} tree[Size];
int n,tot;
inline void Update(int x) { //更新子樹大小
tree[x].siz=tree[lc(x)].siz+tree[rc(x)].siz+1;
}
inline int New(int v) { //新建節點並返回其下標
tree[++tot].val=v;
tree[tot].pri=rand();
tree[tot].siz=1;
return tot;
}
int Merge(int x,int y) {
//合併以x和y爲根的樹
if(!x || !y) return x+y;
//用優先級決定新的根是x還是y
if(tree[x].pri<tree[y].pri) {
rc(x)=Merge(rc(x),y);
Update(x);
return x;
} else {
lc(y)=Merge(x,lc(y));
Update(y);
return y;
}
}
void Split(int i,int k,int &x,int &y) {
//把以i爲根的樹分成以x爲根和以y爲根的樹
//把所有權值<=k的分到x中,>k的分到y中
if(!i) {
//若i爲空節點,則x和y也爲空
x=y=0;
} else {
if(tree[i].val<=k) {
//這裏先讓x=i,然後遞歸下去就把i的右子樹中<=k的分到了rc(x)
x=i;
Split(rc(i),k,rc(x),y);
} else {
//同理
y=i;
Split(lc(i),k,x,lc(y));
}
Update(i); //記得更新子樹大小
}
}
int kth(int i,int k) {
//找到i的子樹中val第k大的點的下標
while(true) {
if(k<=tree[lc(i)].siz) {
//若k比左子樹大小還小
//則第k大顯然在左子樹中
i=lc(i);
} else if(k>tree[lc(i)].siz+1) {
//同理
k-=tree[lc(i)].siz+1;
i=rc(i);
} else {
return i;
}
}
}
inline int Get_K(int rt,int rk) {
//得到第k大的點的權值
return tree[kth(rt,rk)].val;
}
int main() {
//暴力**不可避
srand(19260817);
n=read();
int root=0;
int x=0,y=0,z=0;
while(n--) {
int op=read();
int num=read();
if(op==1) {
//插入num
Split(root,num,x,y);
root=Merge(Merge(x,New(num)),y);
} else if(op==2) {
//刪除num
Split(root,num,x,z);
//此時x中存了所有權值<=num的點
Split(x,num-1,x,y);
//此時x中存了所有權值<=num-1的點
//因此y中存了所有權值爲num的點
//然後把y的根節點刪掉
y=Merge(lc(y),rc(y));
root=Merge(Merge(x,y),z);
} else if(op==3) {
//查詢num的排名
//x中存的是所有權值<=num-1的點
Split(root,num-1,x,y);
//因此x的大小+1就是num的排名
printf("%d\n",tree[x].siz+1);
root=Merge(x,y);
} else if(op==4) {
//查詢第num小的數
printf("%d\n",Get_K(root,num));
} else if(op==5) {
//查詢num的前驅
//x存的是所有權值<=num-1的點
//x中第(x子樹大小)小的點就是num前驅
Split(root,num-1,x,y);
printf("%d\n",Get_K(x,tree[x].siz));
root=Merge(x,y);
} else {
//x的後繼(基本同理)
//y存的是所有權值>num的點
//因此y中第一小的就是num前驅
Split(root,num,x,y);
printf("%d\n",Get_K(y,1));
root=Merge(x,y);
}
}
return 0;
}