FHQ Theap 学习笔记
1. Idea
功能强大的无旋 Treap,而且一听名字就像是我的本命数据结构(
核心思想在于分裂与合并,通过对于每个节点赋予一个随机的优先级,使得该平衡树可以舍弃 Treap 和 Splay 用来保证复杂度的旋转。
对于每个节点,我们一般需要维护左儿子 ,右儿子 ,子树大小 ,权值 和随机优先级 。
值得一提的是,一般情况下,FHQ Treap 会将相同的权值看作不同的节点,因此不需要对于每个节点专门记录出现的次数 。
2. Operation
2.1 分裂
核心操作之一,主要是对于一棵平衡树 ,将它根据权值 ,分裂为两个树 ,使得对于任意节点 ,均有 。
那么考虑当前遍历到节点 ,若 ,就把 以及 的左子树放进 ,然后继续在 里查找;若 ,就把 以及 的右子树放进 ,然后继续在 里查找。具体实现时可以在递归的时候传入两个地址 ,分别表示如果要放进 和 的话要放在哪位置。见代码:。
void split(int p,int v,int &x,int &y){ //当前分裂节点,分裂权值,放入Tx的位置以及放入Ty的位置
if (!p){
x=y=0;
return;
}
if (v<val[p]) split(lson[p],v,x,lson[y=p]);
else split(rson[p],v,rson[x=p],y);
pushup(p);
}
其中 pushup
主要是用于更新节点,可以通过左右儿子更新当前节点的 等
上面的分裂操作是根据树的权值分裂,在维护序列时,也可以根据树的大小分裂,具体原理相似。
void split(int p,int v,int &x,int &y){
if (!p){
x=y=0;
return;
}
if (v<=size[lson[p]]) split(lson[p],v,x,lson[y=p]);
else split(rson[p],v-size[lson[p]]-1,rson[x=p],y);
pushup(p);
}
2.2 合并
另一个核心操作,可以算是分裂的逆操作(废话)。主要是对于两棵平衡树 ,如果满足存在一个值 ,使得对于任意节点 ,均有 ,那么我们就可以合并这两棵树。具体来说,若当前合并到节点 ,若 ,则将 作为根节点,让 和 合并后的结果作为节点 的新右儿子;反之,则将 作为根节点,让 和 合并后的结果作为节点 的新左儿子。
类似于线段树合并,当 有一个为空时,返回两者之和即可。
int merge(int x,int y){
if (!x||!y) return x|y;
if (rd[x]>rd[y]){
rson[x]=merge(rson[x],y);
pushup(x);
return x;
}else{
lson[y]=merge(x,lson[y]);
pushup(y);
return y;
}
}
2.3 新建节点
一个称不上操作的操作。
int newnode(int v){
int x=++cnt;
size[x]=1,rd[x]=rand(),val[x]=v;
return x;
}
2.4 查询排名为 k 的数
直接在平衡树上二分(?,对于当前节点 ,若 ,则继续查找左儿子;若 ,则 即为答案;否则将 减去 后,继续查找右儿子。
int rnk(int gen,int x){
int now=gen;
while (1){
if (size[lson[now]]>=x) now=lson[now];
else if (size[lson[now]]+1==x) return val[now];
else x-=size[lson[now]]+1,now=rson[now];
}
}
2.5 添加节点
新建一个节点后与原树合并即可。
2.6 删除节点
设删除的权值为 ,则可以将原树先按照 分裂为 和 ,在按照 将 分裂为 和 ,那么此时 上的节点权值都为 ,于是删除 上的一个节点即可。具体来说,可以将 的左儿子和右儿子合并,再将 合并即可。
2.7 查询 p 的排名
将平衡树以权值 进行分裂,那么分裂后 的大小加上 后即为 的排名。
2.8 查询 p 的前驱
将平衡树按照 分裂为两棵树 ,则前驱就是 中排名第 的数,输出后再将 合并即可。
2.9 查询 p 的后继
将平衡树按照 分裂为两棵树 ,则后继就是 中排名第 的数,输出后再将 合并即可。
2.10 区间翻转
在用 FHQ Treap 维护区间信息时,对于区间 的翻转操作,可以先将平衡树按照大小 分裂为 ,在按照大小 将 分裂为 和 ,然后在 的根节点 上打上区间翻转标记后再将 合并即可。对于以后的每一次操作,若当前节点被标记,记得操作前先翻转左右儿子,下传并清空标记。
注意,由于是文艺平衡树是按照区间作为区分左右儿子的标准而非普通平衡树的 ,而其 的作用只是表示一个答案,故分裂的时候需要按照树的大小分裂,所以一定要注意先分裂 在分裂 ,不然树的大小就错了(血的教训)。
3. Example
3.1 P3369 【模板】普通平衡树
板子题,将上面的一些操作拼接起来即可。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define N 100005
int n,ls[N],rs[N],va[N],sz[N],rd[N],cnt,rt;
ll read(){
ll wh=0,fh=1;
char c=getchar();
while (c>'9'||c<'0'){
if (c=='-') fh=-1;
c=getchar();
}
while (c>='0'&&c<='9'){
wh=(wh<<3)+(wh<<1)+(c^48);
c=getchar();
}
return wh*fh;
}
void pushup(int x){
sz[x]=sz[ls[x]]+sz[rs[x]]+1;
}
int merge(int x,int y){
if (!x||!y) return x^y;
if (rd[x]>rd[y]){
rs[x]=merge(rs[x],y);
pushup(x);
return x;
}else{
ls[y]=merge(x,ls[y]);
pushup(y);
return y;
}
}
void split(int p,int v,int &x,int &y){
if (!p){
x=y=0;
return;
}
if (v<va[p]) split(ls[p],v,x,ls[y=p]);
else split(rs[p],v,rs[x=p],y);
pushup(p);
}
int newnode(int v){
int x=++cnt;
rd[x]=rand(),va[x]=v,sz[x]=1;
return x;
}
int rnk(int gen,int x){
int now=gen;
while (1){
if (sz[ls[now]]>=x) now=ls[now];
else if (sz[ls[now]]+1==x) return va[now];
else x-=sz[ls[now]]+1,now=rs[now];
}
}
int main(){
srand(time(0));
n=read();
while (n--){
int op=read(),x=read();
if (op==1){
int a=0,b=0;
split(rt,x-1,a,b),rt=merge(a,merge(newnode(x),b));
}else if (op==2){
int a=0,b=0,c=0;
split(rt,x-1,a,c),split(c,x,b,c),rt=merge(merge(a,b=merge(ls[b],rs[b])),c);
}else if (op==3){
int a=0,b=0;
split(rt,x-1,a,b),printf("%d\n",sz[a]+1),rt=merge(a,b);
}else if (op==4){
printf("%d\n",rnk(rt,x));
}else if (op==5){
int a=0,b=0;
split(rt,x-1,a,b),printf("%d\n",rnk(a,sz[a])),rt=merge(a,b);
}else{
int a=0,b=0;
split(rt,x,a,b),printf("%d\n",rnk(b,1)),rt=merge(a,b);
}
}
return 0;
}
3.2 P3391 【模板】文艺平衡树
FHQ Treap 维护区间的板子题。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define N 100005
int n,m,lson[N],rson[N],size[N],rd[N],lz[N],rt,cnt,val[N];
ll read(){
ll wh=0,fh=1;
char c=getchar();
while (c>'9'||c<'0'){
if (c=='-') fh=-1;
c=getchar();
}
while (c>='0'&&c<='9'){
wh=(wh<<3)+(wh<<1)+(c^48);
c=getchar();
}
return wh*fh;
}
int newnode(int v){
int x=++cnt;
size[x]=1,rd[x]=rand(),val[x]=v;
return x;
}
void pushdown(int p){
if (lz[p]){
swap(lson[p],rson[p]);
lz[lson[p]]^=1,lz[rson[p]]^=1;
lz[p]=0;
}
}
void pushup(int p){
size[p]=size[lson[p]]+size[rson[p]]+1;
}
int merge(int x,int y){
if (!x||!y) return x|y;
if (rd[x]>rd[y]){
pushdown(x);
rson[x]=merge(rson[x],y);
pushup(x);
return x;
}else{
pushdown(y);
lson[y]=merge(x,lson[y]);
pushup(y);
return y;
}
}
void split(int p,int v,int &x,int &y){
if (!p){
x=y=0;
return;
}
pushdown(p);
if (v<=size[lson[p]]) split(lson[p],v,x,lson[y=p]);
else split(rson[p],v-size[lson[p]]-1,rson[x=p],y);
pushup(p);
}
void print(int now){
pushdown(now);
if (lson[now]) print(lson[now]);
printf("%d ",val[now]);
if (rson[now]) print(rson[now]);
}
int main(){
n=read(),m=read();
for (int i=1;i<=n;i++) rt=merge(rt,newnode(i));
for (int i=1;i<=m;i++){
int l=read(),r=read();
int a=0,b=0,c=0;
split(rt,r,a,c),split(a,l-1,a,b);
// split(rt,l-1,a,b),split(b,r,b,c);
lz[b]^=1;
rt=merge(a,merge(b,c));
}
print(rt);
return 0;
}
3.3 P2042 [NOI2005] 维护数列
算是文艺平衡树 2.0 版本,细节巨多。
插入删除操作就不说了,主要说说后面四个操作。
- 查询区间和
对于每一个节点多维护一个子树和 ,每次上传更新时将 顺便更新即可。
- 查询最大子段和
类似于带修最大子段和的线段树做法,我们可以对于平衡树的每一个节点维护一个前缀最大值 ,后缀最大值 ,和最大子段和 ,规定 可以为空, 不能为空(题目要求,当然也有别的写法),在上传更新时,对于节点 :
仔细想想很好理解。
注意若某个儿子为空,则转移 时不要转移该儿子,否则就可能会将 赋值为 。
- 区间覆盖操作
其实原理很简单,假设要将区间 覆盖为 ,则将平衡树按照 , 分裂成三棵,将中间一棵取出后在根节点出打上覆盖标记,并修改根节点要维护的值,将 设为该值, 设为该值与子树大小的积, 设为 的较大值, 设为 与该值的较大值(不能为空)。值得一提的是,修改根节点的维护一定要在打标记时修改而非下传标记时修改,因为查询该节点的答案时不一定下传了标记。
- 区间翻转操作
维护区间翻转标记。每次翻转时记得交换当前节点的 和 ,注意在打标记的时候就要完成这一交换, 理由同上。
- 回收节点
注意到插入节点的总数与同时存在的节点数是不对等的,如果一味的插入节点会导致空间严重不足。于是可以考虑建立节点回收,删除节点后记录编号下次再次使用。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define N 500005
int n,m,lson[N],size[N],rson[N],val[N],rd[N],flz[N],lz[N],sum[N],qsum[N],qz[N],hz[N],cnt,hs[N],htot,rt;
char op[20];
ll read(){
ll wh=0,fh=1;
char c=getchar();
while (c>'9'||c<'0'){
if (c=='-') fh=-1;
c=getchar();
}
while (c>='0'&&c<='9'){
wh=(wh<<3)+(wh<<1)+(c^48);
c=getchar();
}
return wh*fh;
}
int newnode(int v){
int x=(htot==0)?++cnt:hs[htot--];
val[x]=qsum[x]=sum[x]=v,size[x]=1,rd[x]=rand(),flz[x]=1e9;
qz[x]=hz[x]=max(0,v);
return x;
}
void pushdown(int x){
if (flz[x]!=1e9){
flz[lson[x]]=flz[rson[x]]=val[lson[x]]=val[rson[x]]=flz[x];
sum[lson[x]]=flz[x]*size[lson[x]],sum[rson[x]]=flz[x]*size[rson[x]];
qz[lson[x]]=hz[lson[x]]=max(sum[lson[x]],0);
qz[rson[x]]=hz[rson[x]]=max(sum[rson[x]],0);
qsum[lson[x]]=max(sum[lson[x]],flz[x]);
qsum[rson[x]]=max(sum[rson[x]],flz[x]);
flz[x]=1e9;
}
if (lz[x]){
lz[lson[x]]^=1,lz[rson[x]]^=1;
swap(hz[lson[x]],qz[lson[x]]),swap(hz[rson[x]],qz[rson[x]]);
swap(lson[x],rson[x]);
lz[x]=0;
}
}
void pushup(int x){
size[x]=size[lson[x]]+size[rson[x]]+1;
sum[x]=sum[lson[x]]+sum[rson[x]]+val[x];
qz[x]=max(max(qz[lson[x]],0),sum[lson[x]]+val[x]+qz[rson[x]]);
hz[x]=max(max(hz[rson[x]],0),sum[rson[x]]+val[x]+hz[lson[x]]);
qsum[x]=hz[lson[x]]+val[x]+qz[rson[x]];
if (lson[x]) qsum[x]=max(qsum[x],qsum[lson[x]]);
if (rson[x]) qsum[x]=max(qsum[x],qsum[rson[x]]);
}
int merge(int x,int y){
if (!x||!y) return x|y;
if (rd[x]>rd[y]){
pushdown(x);
rson[x]=merge(rson[x],y);
pushup(x);
return x;
}else{
pushdown(y);
lson[y]=merge(x,lson[y]);
pushup(y);
return y;
}
}
void split(int p,int v,int &x,int &y){
if (!p){
x=y=0;
return;
}
pushdown(p);
if (v>size[lson[p]]) split(rson[p],v-size[lson[p]]-1,rson[x=p],y);
else split(lson[p],v,x,lson[y=p]);
pushup(p);
}
void dele(int x){
size[x]=lson[x]=rson[x]=val[x]=sum[x]=hz[x]=qz[x]=qsum[x]=lz[x]=flz[x]=0;
hs[++htot]=x;
}
void pop(int u){
if (lson[u]) pop(lson[u]);
if (rson[u]) pop(rson[u]);
dele(u);
}
int main(){
srand(time(0));
n=read(),m=read();
for (int i=1;i<=n;i++) rt=merge(rt,newnode(read()));
for (int i=1;i<=m;i++){
scanf("%s",op);
if (op[2]=='S'){
int pos=read(),tot=read(),a=0,b=0;
split(rt,pos,a,b);
for (int j=1;j<=tot;j++){
a=merge(a,newnode(read()));
}
rt=merge(a,b);
}else if (op[2]=='L'){
int pos=read(),tot=read(),a=0,b=0,c=0;
split(rt,pos+tot-1,b,c),split(b,pos-1,a,b);
rt=merge(a,c),pop(b);
}else if (op[2]=='K'){
int pos=read(),tot=read(),w=read(),a=0,b=0,c=0;
split(rt,pos+tot-1,b,c),split(b,pos-1,a,b);
flz[b]=val[b]=w,sum[b]=w*size[b],qz[b]=hz[b]=max(0,sum[b]),qsum[b]=max(qsum[b],w);
rt=merge(merge(a,b),c);
}else if (op[2]=='V'){
int pos=read(),tot=read(),a=0,b=0,c=0;
split(rt,pos+tot-1,b,c),split(b,pos-1,a,b);
lz[b]^=1;
swap(qz[b],hz[b]);
rt=merge(merge(a,b),c);
}else if (op[2]=='T'){
int pos=read(),tot=read(),a=0,b=0,c=0;
split(rt,pos+tot-1,b,c),split(b,pos-1,a,b);
printf("%d\n",sum[b]);
rt=merge(merge(a,b),c);
}else{
printf("%d\n",qsum[rt]);
}
}
return 0;
}
3.4 P3165 [CQOI2014]排序机械臂
总结一下题意发现其实就是每次找出序列中的最小值,删去之后将其前面的序列翻转。而两者恰好都可以使用 FHQ Treap 维护。找序列的最小值只需要维护子树内最小值,查询时在平衡树上二分即可,二分时相当于是在查找最小值在序列中位置的排名。
但是注意题目要求若两个元素相同,输出的元素位置顺序要和初始位置顺序一样,所以我们还需要额外记录一个 表示子树内最小值的最小输入编号,然后平衡树上二分时将 作为第二关键字即可。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define N 100005
int n,p[N],size[N],minn[N],val[N],lson[N],rson[N],rd[N],flz[N],rt,cnt,imin[N];
ll read(){
ll wh=0,fh=1;
char c=getchar();
while (c>'9'||c<'0'){
if (c=='-') fh=-1;
c=getchar();
}
while (c>='0'&&c<='9'){
wh=(wh<<3)+(wh<<1)+(c^48);
c=getchar();
}
return wh*fh;
}
void pushdown(int x){
if (flz[x]){
flz[lson[x]]^=1,flz[rson[x]]^=1;
swap(lson[x],rson[x]);
flz[x]=0;
}
}
void pushup(int x){
size[x]=size[lson[x]]+size[rson[x]]+1;
minn[x]=val[x],imin[x]=x;
if (lson[x])
if (minn[lson[x]]<minn[x]) minn[x]=minn[lson[x]],imin[x]=imin[lson[x]];
else if (minn[lson[x]]==minn[x]&&imin[lson[x]]<imin[x]) imin[x]=imin[lson[x]];
if (rson[x])
if (minn[rson[x]]<minn[x]) minn[x]=minn[rson[x]],imin[x]=imin[rson[x]];
else if (minn[rson[x]]==minn[x]&&imin[rson[x]]<imin[x]) imin[x]=imin[rson[x]];
}
int merge(int x,int y){
if (!x||!y) return x|y;
if (rd[x]>rd[y]){
pushdown(x);
rson[x]=merge(rson[x],y);
pushup(x);
return x;
}else{
pushdown(y);
lson[y]=merge(x,lson[y]);
pushup(y);
return y;
}
}
void split(int p,int v,int &x,int &y){
if (!p){
x=y=0;
return;
}
pushdown(p);
if (v<=size[lson[p]]) split(lson[p],v,x,lson[y=p]);
else split(rson[p],v-size[lson[p]]-1,rson[x=p],y);
pushup(p);
}
int newnode(int v){
int x=++cnt;
val[x]=minn[x]=v,size[x]=1,rd[x]=rand(),imin[x]=x;
return x;
}
int find(int now){
int sum=0;
while (1){
pushdown(now);
if (lson[now]&&minn[lson[now]]==minn[now]&&imin[lson[now]]==imin[now]) now=lson[now];
else if (val[now]==minn[now]&&imin[now]==now) return sum+size[lson[now]]+1;
else sum+=size[lson[now]]+1,now=rson[now];
}
}
int main(){
srand(time(0));
n=read();
for (int i=1;i<=n;i++) rt=merge(rt,newnode(read()));
for (int i=1;i<=n;i++){
int now=find(rt),a=0,b=0,c=0;
split(rt,now,b,c),split(b,now-1,a,b);
printf("%d ",now+i-1);
flz[a]^=1;
rt=merge(a,c);
}
return 0;
}
4. Extra
4.1 P3835 【模板】可持久化平衡树
平衡树的可持久化加强版。
没什么好说的,观察发现普通的 FHQ Theap 只有在分裂以及合并的时候需要修改节点信息,于是我们就可以将原来 merge 和 split 修改信息的过程改为新建节点,然后将新建的节点放到新的版本中即可。修改的幅度不大,代码很好写。
函数:
int merge(int x,int y){
if (!x||!y) return x|y;
if (rnd[x]>rnd[y]){
int k=chan(x);//将x复制一份给k
rson[k]=merge(rson[k],y);
pushup(k);
return k;
}else{
int k=chan(y);
lson[k]=merge(x,lson[k]);
pushup(k);
return k;
}
}
函数:
void split(int p,int v,int &x,int &y){
if (!p){
x=y=0;
return;
}
int k=chan(p);
if (v<val[k]) split(lson[k],v,x,lson[y=k]);
else split(rson[k],v,rson[x=k],y);
pushup(k);
}
4.2 P5055 【模板】可持久化文艺平衡树
文艺平衡树的合并与分裂操作的可持久化自然和普通平衡树十分相似(除了将普通平衡树的按权值分裂变为按照大小分裂),不过除此之外,还需要注意翻转标记的可持久化。
正常的 FHQ Treap 在标记翻转的时候,是先将原树分裂成三份,然后将中间的树根标记后再将三份树合并。但是可持久化后,显然不能再直接标记,而是需要将中间的树根新建一个替身节点,再给新建节点打上标记。但是此时我们其实是少可持久化了一些节点的,也就是新建节点的子树节点,这些节点由于会被翻转,也需要新建一个版本记录,而懒标记却将这个记录延后了。所以当我们下传标记的时候,还需要在儿子收到下传标记之前先新建一个版本,然后再下传,这样就不会覆盖原有的版本了。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define N 200005
#define M N<<7
ll n,cnt,val[M],sum[M],la;
int lson[M],rson[M],rnd[M],size[M],lz[M],gen[M];
ll read(){
ll wh=0,fh=1;
char c=getchar();
while (c>'9'||c<'0'){
if (c=='-') fh=-1;
c=getchar();
}
while (c>='0'&&c<='9'){
wh=(wh<<3)+(wh<<1)+(c^48);
c=getchar();
}
return wh*fh;
}
int newnode(int v){
int x=++cnt;
val[x]=sum[x]=v,size[x]=1,rnd[x]=rand();
return x;
}
int chan(int p){
int x=++cnt;
val[x]=val[p],size[x]=size[p],sum[x]=sum[p],lson[x]=lson[p],rnd[x]=rnd[p],rson[x]=rson[p],lz[x]=lz[p];
return x;
}
void pushdown(int p){
if (lz[p]){
if (lson[p]) lz[lson[p]=chan(lson[p])]^=1;
if (rson[p]) lz[rson[p]=chan(rson[p])]^=1;
swap(lson[p],rson[p]);
lz[p]=0;
}
}
void pushup(int p){
size[p]=size[lson[p]]+size[rson[p]]+1;
sum[p]=sum[lson[p]]+sum[rson[p]]+val[p];
}
int merge(int x,int y){
if (!x||!y) return x|y;
if (rnd[x]>rnd[y]){
pushdown(x);
int k=chan(x);
rson[k]=merge(rson[k],y);
pushup(k);
return k;
}else{
pushdown(y);
int k=chan(y);
lson[k]=merge(x,lson[k]);
pushup(k);
return k;
}
}
void split(int p,int v,int &x,int &y){
if (!p){
x=y=0;
return;
}
pushdown(p);
int k=chan(p);
if (v<=size[lson[k]]) split(lson[k],v,x,lson[y=k]);
else split(rson[k],v-size[lson[k]]-1,rson[x=k],y);
pushup(k);
return;
}
int main(){
srand(time(0));
n=read();
for (int i=1;i<=n;i++){
int p=read(),op=read();
if (op==1){
int x=read()^la,y=read()^la,a=0,b=0;
split(gen[p],x,a,b),gen[i]=merge(merge(a,newnode(y)),b);
}else if (op==2){
int x=read()^la,a=0,b=0,c=0;
split(gen[p],x,b,c),split(b,x-1,a,b);
gen[i]=merge(a,c);
}else if (op==3){
int l=read()^la,r=read()^la,a=0,b=0,c=0;
split(gen[p],r,b,c),split(b,l-1,a,b);
int w=chan(b);
lz[w]^=1;
gen[i]=merge(merge(a,w),c);
}else{
int l=read()^la,r=read()^la,a=0,b=0,c=0;
split(gen[p],r,b,c),split(b,l-1,a,b);
printf("%lld\n",la=sum[b]);
gen[i]=gen[p];
}
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】