FHQ Treap(無旋 Treap)
簡介
FHQ Treap,也稱無旋Treap,是范浩強神犇發明的一種平衡樹,我認為這是最好寫,最簡短,最清晰的平衡樹之一,碼量很小,完全可以在OI限時比賽中使用。它基於分裂(Split)和合並(Merge)操作,使得二叉查找樹的形態趨近平衡
實現
存儲與維護
和有旋Treap一樣,無旋Treap同樣需要在每一個節點中存儲一個隨機值,在合並時會使用到隨機值
也就是說無旋Treap需要維護:兩個子節點編號,子樹大小,隨機值的維護的值
struct Node
{
int ch[2],v,rnd,siz;
//分別為子節點編號(0代表左兒子,1代表右兒子),維護的值,隨機值和子樹大小
};
更新沒什么好說的,就是把自己的子樹大小更新為子節點子樹大小之和再加一
void update(int x)
{
node[x].siz=node[node[x].ch[0]].siz+node[node[x].ch[1]].siz+1;
}
分裂
分裂可以將完整的一棵平衡樹分裂為兩棵平衡樹
分裂是無旋Treap的基礎操作之一,分為按照權值分裂和按照 \(size\) 分裂
按權值分裂
按權值分裂需要從根節點開始遍歷
如果當前節點的權值小於等於要分裂的權值,就說明以當前節點左兒子為根的子樹和當前節點都需要被分裂到左邊的樹,那么我們就把傳入函數的 \(x\) 更改為當前正在遍歷節點的編號,然后進入右兒子繼續分裂
否則就說明要分裂出去的節點全部在左子樹中,我們就更改傳入的 \(y\) 然后進入左兒子分裂
找到空節點就返回
void vsplit(int pos,int v,int &x,int &y)
{
if(!pos)//空節點
{
x=y=0;
return;
}
if(node[pos].v<=v) x=pos,vsplit(node[pos].ch[1],v,node[pos].ch[1],y);//進入右兒子
else y=pos,vsplit(node[pos].ch[0],v,x,node[pos].ch[0]);//進入左兒子
update(pos);//更新節點信息
}
按 \(size\) 分裂
按 \(size\) 分裂與其它平衡樹中查找排名為 \(k\) 的數值的方法類似,從根節點開始
要分裂的 \(size\) 如果比當前節點的左兒子的 \(size\) 大,就說明以當前節點左兒子為根的子樹和當前節點都需要被分裂到左邊的樹,那么就減去左兒子的 \(size+1\) (當前節點)然后進入右兒子繼續分裂
否則就進左兒子分裂
最后找到空節點就返回
void ssplit(int pos,int k,int &x,int &y)
{
if(!pos)//空節點
{
x=y=0;
return;
}
if(k>node[node[pos].ch[0]].siz)//減去左子樹大小+1后進入右兒子
x=pos,ssplit(node[pos].ch[1],k-node[node[pos].ch[0]].siz-1,node[pos].ch[1],y);
else y=pos,ssplit(node[pos].ch[0],k,x,node[pos].ch[0]);//進入左兒子
update(pos);//更新節點信息
}
合並
合並利用遞歸實現,若合並的任意一個子樹為空,那么就直接返回另一個節點,我們可以用 \(x+y\) 方便地做到這一點
然后我們比較兩個節點的隨機值大小,根據隨機值大小關系把第一個節點與第二個節點的子節點合並或者把第二個節點與第一個節點的子節點合並
int merge(int x,int y)
{
if(!x||!y) return x+y;//有節點為空
if(node[x].rnd<node[y].rnd)
{
node[x].ch[1]=merge(node[x].ch[1],y);//把第一個節點的右兒子與第二個節點合並
update(x);//更新節點信息
return x;//返回新的根
}
node[y].ch[0]=merge(x,node[y].ch[0]);//把第一個節點和第二個節點的左兒子合並
update(y);//更新節點信息
return y;//返回新的根
}
新建節點
新建節點的之后注意要賦初始值,不要忘記了
int cnt;
int newNode(int x)
{
node[++cnt].rnd=rand(),node[cnt].v=x,node[cnt].siz=1;
return cnt;
}
插入
有了分裂與合並,無旋Treap的幾乎所有操作實現都非常簡單
插入操作只需要用按權值分裂把權值比插入值小的和大的節點分裂,然后合並這兩個子樹和新的節點即可
void insert(int v)
{
int x,y;
vsplit(Root,v,x,y);//按權值分裂
Root=merge(merge(x,newNode(v)),y);//合並
}
刪除
刪除時我們把小於等於刪除值的子樹分裂出來,再把小於刪除值的子樹分分裂,得到的就是三棵子樹,其中一棵只含有待刪除值,我們合並這課子樹的左右子節點,然后把新得到的子樹和另外兩棵子樹合並就能實現刪除操作
void erase(int v)
{
int x,y,z;
vsplit(Root,v,x,z);//分裂小於等於v的
vsplit(x,v-1,x,y);//分裂小於v的
y=merge(node[y].ch[0],node[y].ch[1]);//合並左右子節點
Root=merge(merge(x,y),z);//合並三棵樹
}
求前驅
把權值小於給定值的節點分裂,在這棵子樹中一直往右走找最大值就是前驅,最后合並然后返回答案
int pre(int v)
{
int x,y,cur;
vsplit(Root,v-1,x,y);//分裂小於v的
cur=x;
while(node[cur].ch[1]) cur=node[cur].ch[1];//一直往右走
merge(x,y);//合並
return node[cur].v;//返回答案
}
求后繼
把權值小於等於給定值的節點分裂,在另一棵子樹中一直往左走找最小值就是后繼,最后合並然后返回答案
int nxt(int v)
{
int x,y,cur;
vsplit(Root,v,x,y);//分裂小於等於v的
cur=y;
while(node[cur].ch[0]) cur=node[cur].ch[0];//一直往左走
merge(x,y);//合並
return node[cur].v;//返回答案
}
查排名
把權值小於給定值的節點分裂,這棵子樹的節點數加一就是排名
需要注意的是一般平衡樹為了防止越界都會一開始插入一個權值無窮大和一個權值無窮小的節點,在處理排名問題的時候需要考慮清楚,查排名時因為有極小值存在,所以我們這里不用加一就是正確答案,自己寫的時候要看清楚
int get_rank(int v)
{
int x,y,ans;
vsplit(Root,v-1,x,y);//分裂小於v的
ans=node[x].siz;//因為有極小值所以不用再加一
merge(x,y);//查完之后記得合並
return ans;
}
查排名為 \(k\) 的數
按照 \(size\) 分裂出 \(k\) 個節點, 分裂出的子樹中一直往右兒子走找到的最大值就是答案
同樣需要注意這里我們因為插入了極小值,所以 \(k\) 在傳入的時候就需要加上一
int kth(int k)
{
++k;//因為極小值的存在需要加一
int x,y,cur;
ssplit(Root,k,x,y);//按size分裂
cur=x;
while(node[cur].ch[1]) cur=node[cur].ch[1];//一直往右走
merge(x,y);//合並
return node[cur].v;//返回答案
}
初始化
初始化的時候注意設置一下隨機種子,把根節點和節點數量賦值為 \(0\) 並且插入極小值和極大值
void init()
{
srand(time(0));
Root=cnt=0;
insert(-INF),insert(INF);
}
封裝
我使用c++的模板以及結構體封裝了一個無旋Treap,帶有大部分的平衡樹操作和內存回收
code
struct Treap
{
const int INF;
int Root,cnt;
deque<int>del_list;
struct Node
{
int ch[2],v,rnd,siz;
}node[N];
int newNode(int x)//申請新節點
{
int tmp;
if(del_list.empty()) tmp=++cnt;
else tmp=del_list.front(),del_list.pop_front();
node[tmp].rnd=rand(),node[tmp].v=x,node[tmp].siz=1,node[tmp].ch[0]=node[tmp].ch[1]=0;
return tmp;
}
void update(int x)//更新信息
{
node[x].siz=node[node[x].ch[0]].siz+node[node[x].ch[1]].siz+1;
}
void vsplit(int pos,int v,int &x,int &y)//按權值分裂
{
if(!pos)
{
x=y=0;
return;
}
if(node[pos].v<=v) x=pos,vsplit(node[pos].ch[1],v,node[pos].ch[1],y);
else y=pos,vsplit(node[pos].ch[0],v,x,node[pos].ch[0]);
update(pos);
}
void ssplit(int pos,int k,int &x,int &y)//按size分裂
{
if(!pos)
{
x=y=0;
return;
}
if(k>node[node[pos].ch[0]].siz)
x=pos,ssplit(node[pos].ch[1],k-node[node[pos].ch[0]].siz-1,node[pos].ch[1],y);
else y=pos,ssplit(node[pos].ch[0],k,x,node[pos].ch[0]);
update(pos);
}
int merge(int x,int y)//合並
{
if(!x||!y) return x+y;
if(node[x].rnd<node[y].rnd)
{
node[x].ch[1]=merge(node[x].ch[1],y);
update(x);
return x;
}
node[y].ch[0]=merge(x,node[y].ch[0]);
update(y);
return y;
}
void insert(int v)//插入
{
int x,y;
vsplit(Root,v,x,y);
Root=merge(merge(x,newNode(v)),y);
}
void erase(int v)//刪除
{
int x,y,z;
vsplit(Root,v,x,z);
vsplit(x,v-1,x,y);
del_list.push_back(y);
y=merge(node[y].ch[0],node[y].ch[1]);
Root=merge(merge(x,y),z);
}
int pre(int v)//前驅
{
int x,y,cur;
vsplit(Root,v-1,x,y);
cur=x;
while(node[cur].ch[1]) cur=node[cur].ch[1];
merge(x,y);
return node[cur].v;
}
int nxt(int v)//后繼
{
int x,y,cur;
vsplit(Root,v,x,y);
cur=y;
while(node[cur].ch[0]) cur=node[cur].ch[0];
merge(x,y);
return node[cur].v;
}
int get_rank(int v)//查排名
{
int x,y,ans;
vsplit(Root,v-1,x,y);
ans=node[x].siz;
merge(x,y);
return ans;
}
int kth(int k)//查排名為k的數
{
++k;
int x,y,cur;
ssplit(Root,k,x,y);
cur=x;
while(node[cur].ch[1]) cur=node[cur].ch[1];
merge(x,y);
return node[cur].v;
}
Treap():INF(2147483647)//構造函數初始化
{
Root=cnt=0;
insert(-INF),insert(INF);
}
};
食用方法
將上方代碼加入您的代碼中,定義時您需要給定一個參數 \(N\) 表示定義的無旋Treap最多有多少個節點,因為添加了內存回收功能,所以如果有大量刪除操作,您不需要定義過多的節點數就能裝下,具體的使用栗子如下
//粘貼封裝好的代碼之后
Treap<100005>a;//定義一棵至多有100005個節點的無旋Treap
Treap<114514>b[10];//定義一個長度為10的數組,每一個位置有一棵至多有114514個節點的無旋Treap
該文為本人原創,轉載請注明出處