浅谈 FHQ-Treap
本文同步发表在洛谷博客。
什么是 FHQ-Treap?
平衡树上存放两个信息,权值 \(val\) 以及随机索引 \(key\)。值满足二叉搜索树性质,随机值索引满足堆的性质,通过结合二叉搜索树和二叉堆的性质来使树平衡。至于这里用的是大根堆还是小根堆,不重要。
当权值 \(val\) 的数值情况不可控时,如果保证索引 \(key\) 为随机,树的期望深度为 \(\log n\)。
通常的平衡树维护平衡的方法是旋转,而 FHQ-Treap 则是用分裂和合并来实现的。
详解 FHQ-Treap
FHQ-Treap 存放的节点信息
首先,有必不可少的权值 \(val\) 和随即索引 \(key\)。其次为了维护树的结构,肯定要有左节点编号 \(ls\) 以及右节点编号 \(rs\)。当然,为了求排名之类的东西,我们还会维护一个子树大小 \(sz\)。由于要维护的信息较多,这里采用结构体进行维护。
struct FHQ_Trp{int ls,rs,val,key,sz;}tree[N];
当然,你还需要 \(cnt\) 和 \(root\),分别表示当前节点个数(也就是最大的编号)以及当前这棵平衡树的根节点 \(root\)。
FHQ-Treap 需要用到的操作
创建新节点
很简单。首先将 \(cnt \gets cnt+1\) 然后让这个节点的编号为 \(cnt\)。给定这个节点的 \(val\) 值,为传参进来的 \(k\),然后再用 mt19937 随机生成索引 \(key\),以及要记得初始化子树大小为 \(1\) 哦。
mt19937 num(998244353);
int New_node(int k){
++cnt;
tree[cnt].val=k;
tree[cnt].key=num();
tree[cnt].sz=1;
return cnt;
}
分裂操作
分裂,即 Split,是 FHQ-Treap 中必不可少的一环。
分裂方法有两种:
- 按值分裂:把树拆成两棵树,拆出来的一棵树的值全部小于等于给定的值,另外一棵树的值全部大于给定的值。
- 按大小分裂:把树拆成两棵树,拆出来的一棵树的值全部等于给定的大小,剩余部分在另外一颗树里。
通常来说,当 FHQ-Treap 作为正常平衡树使用时,采用按值分裂;维护区间信息时,用按大小分裂。
这里用按值分裂举例。其实很简单的,根据二叉搜索树的性质,如果当前节点的权值小于等于这个分裂的值,那么左子树肯定都稳了,可右子树还确定不了,因此这个时候就往右边递归;同理,如果当前节点的权值大于这个分裂的值,那么右子树也肯定都稳了,左子树不确定,这个时候就往左递归。更新迭代的时候不要忘了更新分裂出来的两棵树的情况,以及它们的根哦。当然,在更新完成之后需要对当前节点进行一次 upd,因为树的结构会随着分裂的操作而变化,子树大小需要重新维护。
void upd(int u){
tree[u].sz=tree[tree[u].ls].sz+tree[tree[u].rs].sz+1;return;
}
void Split(int u,int k,int &x,int &y){
if(!u){x=0,y=0;return;}
else if(tree[u].val<=k){
x=u;
Split(tree[u].rs,k,tree[u].rs,y);
}else{
y=u;
Split(tree[u].ls,k,x,tree[u].ls);
}upd(u);return;
}
合并操作
合并操作,很简单的,就是把两棵分别以 \(x\) 和 \(y\) 为根的平衡树合并在一起,并要求合并之后仍然满足平衡树的性质。在这里的合并就和之前的分裂不一样了,分裂的时候重点运用的是二叉搜索树的性质,而这里需要重点运用二叉堆的性质,去选择维护的方向。
int Merge(int x,int y){
if(!x||!y)return x+y;
if(tree[x].key>tree[y].key){
tree[x].rs=Merge(tree[x].rs,y);
upd(x);return x;
}else{
tree[y].ls=Merge(x,tree[y].ls);
upd(y);return y;
}
}
值得注意的是,当你的平衡树的分裂操作做的是按大小分裂的时候,即你维护区间情况的时候,一定要注意,\(\text{Merge}(x,y)\) 和 \(\text{Merge}(y,x)\) 是在干两件不同的事情哦!本人被狠狠坑过。
如何维护 FHQ-Treap
插入节点
假设我们要插入的节点的权值为 \(k\)。首先我们要分裂这棵平衡树,把它分裂成两个部分,一个部分的权值 \(\le k\),还有一个部分的权值 \(>k\),即执行 \(\text{Split}(root,k,x,y)\) 操作,其中 \(x\) 和 \(y\) 是你设定的两个变量,用于存储分裂后的树的两个根节点。接着,我们考虑新建一个节点村上这个要插入的值,然后将其和以 \(x\) 为根的树合并,最后再和以 \(y\) 为根的树合并。这样,你就完美地实现了一个插入的过程!
void Insert(int k){
int x=0,y=0;
Split(root,k,x,y);
root=Merge(Merge(x,New_node(k)),y);
return;
}
删除节点
同样假设我们要删除的节点的权值为 \(k\)。首先,仍然对树进行分裂,不过这一次我们将把树分裂成三部分:以 \(x\) 为根节点的部分,所有节点权值 \(< k\);以 \(y\) 为根节点的部分,所有节点权值 \(=k\);以 \(z\) 为根节点的部分,所有节点权值 \(>k\)。这个时候,我们只需要从以 \(y\) 为根节点的树里面删掉一个节点,最后把大家全都合并起来就可以了。如何删掉 \(y\) 中的一个节点?很简单,由于权值都是 \(k\),没多大区别,因此直接把根节点删掉,让 \(y\) 的左子树和右子树合并起来就行了。如果题目特殊要求,说如果存在多个全部删掉,那么最后就只需要合并 \(x\) 和 \(z\) 了,\(y\) 直接扔掉不管就行。
void Delete(int k){
int x=0,y=0,z=0;
Split(root,k,x,z);
Split(x,k-1,x,y);
y=Merge(tree[y].ls,tree[y].rs);
root=Merge(Merge(x,y),z);
return;
}
给定数值查询排名
即根据具体值的大小情况查询排名,如有重复则存在并列情况。假设这个值为 \(k\)。
很简单啦,同样对树进行分裂,不同于以往的 \(\le k\) 以及 \(> k\),这次为了方便求排名我们是按照 \(<k\) 和 \(\ge k\) 去分裂的,然后得到 \(<k\) 的这一部分树的大小,加上一就是答案。最后记得要把树合并回来。
int Get_rank(int k){
int x=0,y=0,rk=0;
Split(root,k-1,x,y);
rk=tree[x].sz+1;
Merge(x,y);return rk;
}
给定排名查询数值
很简单,从根节点开始往下搜索,如果左子树大小 \(+1\) 恰好等于传参进来的 \(rk\)(也就是排名),那么说明当前 \(u\) 就是答案,直接返回即可;否则根据左子树的大小是否足够,判断现在 \(u\) 是移动向左子树还是右子树,同时如果往右子树移的话要注意将 \(rk\) 的值减少对应数量。如果到最后都还没找到就返回 \(-1\),但除非这个排名越界了,否则都是会找到的啦!
int Get_number(int rk){
int u=root;
while(u){
if(tree[tree[u].ls].sz+1==rk)return tree[u].val;
else if(tree[tree[u].ls].sz>=rk)u=tree[u].ls;
else rk-=tree[tree[u].ls].sz+1,u=tree[u].rs;
}return -1;
}
查询数值前驱
数值 \(k\) 的前驱定义为最大的 \(<k\) 的数的具体值,\(k\) 不一定要在树中出现过。
依然很简单,与上面相同的,把树拆成 \(< k\) 和 \(\ge k\) 的两部分,然后从 \(<k\) 的那部分树的根节点找起,只要有右节点,就一直往右边跑(因为这样才能得到最大值),直到没得跑了,直接返回就行了,不过要注意把树合并回来。
int Find_pre(int k){
int x=0,y=0;Split(root,k-1,x,y);
int u=x;while(tree[u].rs)u=tree[u].rs;
Merge(x,y);return u;
}
查询数值后继
数值 \(k\) 的后继定义为最小的 \(>k\) 的数的具体值,\(k\) 不一定要在树种出现过。
和查询前驱是一样的,不同的是,这里是把树拆成 \(\le k\) 和 \(>k\) 两部分,然后从 \(>k\) 的那部分树的根节点找起。当然了,这里是一直往左边跑,因为这样才能得到最小值嘛!同样要记得把树合并回来。
int Find_nxt(int k){
int x=0,y=0;Split(root,k,x,y);
int u=y;while(tree[u].ls)u=tree[u].ls;
Merge(x,y);return u;
}
【模版】普通平衡树
来看板子题,https://www.luogu.com.cn/problem/P3369。
由于其要求支持插入、删除、查询排名、查询数值、查询前驱后继这些操作,正好就是上面所提到的,所以把上面的那些零零散散的代码拼在一起就过了!
为了方便我还是放个完整代码在这里吧。
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
using namespace std;
const int N = 1e5+5;
mt19937 num(998244353);
int Q,cnt,root;
struct FHQ_Trp{int ls,rs,val,key,sz;}tree[N];
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
int New_node(int k){
++cnt;
tree[cnt].val=k;
tree[cnt].key=num();
tree[cnt].sz=1;
return cnt;
}
void upd(int u){
tree[u].sz=tree[tree[u].ls].sz+tree[tree[u].rs].sz+1;return;
}
void Split(int u,int k,int &x,int &y){
if(!u){x=0,y=0;return;}
else if(tree[u].val<=k){
x=u;
Split(tree[u].rs,k,tree[u].rs,y);
}else{
y=u;
Split(tree[u].ls,k,x,tree[u].ls);
}upd(u);return;
}
int Merge(int x,int y){
if(!x||!y)return x+y;
if(tree[x].key>tree[y].key){
tree[x].rs=Merge(tree[x].rs,y);
upd(x);return x;
}else{
tree[y].ls=Merge(x,tree[y].ls);
upd(y);return y;
}
}
void Insert(int k){
int x=0,y=0;
Split(root,k,x,y);
root=Merge(Merge(x,New_node(k)),y);
return;
}
void Delete(int k){
int x=0,y=0,z=0;
Split(root,k,x,z);
Split(x,k-1,x,y);
y=Merge(tree[y].ls,tree[y].rs);
root=Merge(Merge(x,y),z);
return;
}
int Get_rank(int k){
int x=0,y=0,rk=0;
Split(root,k-1,x,y);
rk=tree[x].sz+1;
Merge(x,y);return rk;
}
int Get_number(int rk){
int u=root;
while(u){
if(tree[tree[u].ls].sz+1==rk)return tree[u].val;
else if(tree[tree[u].ls].sz>=rk)u=tree[u].ls;
else rk-=tree[tree[u].ls].sz+1,u=tree[u].rs;
}return -1;
}
int Find_pre(int k){
int x=0,y=0;Split(root,k-1,x,y);
int u=x;while(tree[u].rs)u=tree[u].rs;
Merge(x,y);return u;
}
int Find_nxt(int k){
int x=0,y=0;Split(root,k,x,y);
int u=y;while(tree[u].ls)u=tree[u].ls;
Merge(x,y);return u;
}
int main(){
Q=read();
while(Q--){
int opt=read(),x=read();
if(opt==1)Insert(x);
else if(opt==2)Delete(x);
else if(opt==3)cout<<Get_rank(x)<<"\n";
else if(opt==4)cout<<Get_number(x)<<"\n";
else if(opt==5)cout<<tree[Find_pre(x)].val<<"\n";
else cout<<tree[Find_nxt(x)].val<<"\n";
}
return 0;
}
【模版】文艺平衡树
就是 https://www.luogu.com.cn/problem/P3391 啦!
这里维护的是区间,所以在上面分裂那里要按大小分裂。
问题在于……不管你维护的什么,这里的翻转操作我也不会弄啊!不过其实没多难,就是把左右子树交换 swap 一下。
可是这么暴力弄都是 \(O(n \times m)\) 的,不超时才怪呢。咋搞?
还记得线段树是如何让区间操作的时间复杂度降下来的?你说对了——懒标记!这里也是一样的道理!
在结构体 FHQ_Trp 里多加 \(tag\) 一维,表示这里的标记,每次需要修改,我们就把 \(tag\) 进行反转。涉及子树修改的情况下,我们再进行 push_down 下传标记。
当然了,我们要进行拆树(不然你给谁标记呢?),拆成 \([1,l-1]\)、\([l,r]\)、\([r+1,n]\) 三个部分,然后需要翻转的当然就是中间 \([l,r]\) 的那一部分啦!最后别忘了并回去。
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
using namespace std;
const int N = 1e5+5;
mt19937 num(998244353);
int n,m,cnt,root;
struct FHQ_Trp{int ls,rs,val,key,sz;bool tag;}tree[N];
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
int New_node(int k){
++cnt;
tree[cnt].val=k;
tree[cnt].key=num();
tree[cnt].sz=1;
return cnt;
}
void push_down(int u){
if(!tree[u].tag)return;
swap(tree[u].ls,tree[u].rs);
tree[tree[u].ls].tag^=1;
tree[tree[u].rs].tag^=1;
tree[u].tag=0;return;
}
void upd(int u){
tree[u].sz=tree[tree[u].ls].sz+tree[tree[u].rs].sz+1;return;
}
void Split(int u,int k,int &x,int &y){
if(!u){x=0,y=0;return;}
push_down(u);
if(tree[tree[u].ls].sz+1<=k){
x=u;
Split(tree[u].rs,k-tree[tree[u].ls].sz-1,tree[u].rs,y);
}else{
y=u;
Split(tree[u].ls,k,x,tree[u].ls);
}upd(u);return;
}
int Merge(int x,int y){
if(!x||!y)return x+y;
if(tree[x].key>tree[y].key){
push_down(x);
tree[x].rs=Merge(tree[x].rs,y);
upd(x);return x;
}else{
push_down(y);
tree[y].ls=Merge(x,tree[y].ls);
upd(y);return y;
}
}
void Insert(int k){
int x=0,y=0;
Split(root,k,x,y);
root=Merge(Merge(x,New_node(k)),y);
return;
}
void Delete(int k){
int x=0,y=0,z=0;
Split(root,k,x,z);
Split(x,k-1,x,y);
y=Merge(tree[y].ls,tree[y].rs);
root=Merge(Merge(x,y),z);
return;
}
void Sol(int l,int r){
int x=0,y=0,z=0;
Split(root,r,x,z);
Split(x,l-1,x,y);
tree[y].tag^=1;
root=Merge(Merge(x,y),z);
return;
}
void DFS(int u){
if(!u)return;
push_down(u);
DFS(tree[u].ls);
cout<<tree[u].val<<" ";
DFS(tree[u].rs);
return;
}
int main(){
n=read(),m=read();
for(int i=1;i<=n;i++)Insert(i);
for(int i=1;i<=m;i++){
int l=read(),r=read();
Sol(l,r);
}DFS(root);cout<<"\n";
return 0;
}

浙公网安备 33010602011771号