FHQ Treap(无旋 Treap)详解
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
该文为本人原创,转载请注明出处