Fhq-Treap学习笔记
前言
\(fhq-Treap\),又称\(~\)非旋\(Treap\)
是\(2011\)年中国\(IOI\)国家队选手\(fhq\)提出的一种平衡树。
在定义和结构上与\(treap\)类似,但是改变了\(treap\)利用旋转维护平衡的方式。
很\(nb\)的平衡树,几乎可以支持所有其他平衡树的操作。
而最让我感到惊讶的是,\(fhq-Treap\)所有功能只基于分裂(\(split\))和合并(\(merge\))两个操作。
(\(fhq-Treap\)还可以持久化。
易理解,码量小,但常数略大,可比屑\(Splay\)还是要快的。
所以说单纯的写平衡树还是写\(fhq-Treap\)的要好。
两个核心
先声明一些变量两个函数,和\(Treap\)的更新与建点相同。
const int N=2e5+10;
int rt,cnt;//根节点和点数
int val[N],siz[N],ls[N],rs[N],pfer[N];
//点权,子树大小,左儿子,右儿子,优先值
void updata(int p){
if(!p)return;
siz[p]=siz[ls[p]]+siz[rs[p]]+1;
}
int New(int x){
int p=++cnt;
val[p]=x;siz[p]=1;
ls[p]=rs[p]=0;
pfer[p]=rand()*rand();
return p;
}
我们知道,\(Treap\)是通过旋转来维持堆性质和\(BST\)性质的,
而\(fhq-Treap\)则通过\(merge\)来维护这两性质。(这里维护的是大根堆性质)
但先来看一下\(fhq-Treap\)的\(split\)操作。
假如给定一个值\(k\),如何将值大于\(k\)的和小于等于\(k\)的节点分成两棵平衡树?
分别设这两棵树为\(A\)树和\(B\)树。假设我们找到一个节点\(p\),分两种情况讨论:
- \(val_p \leqslant k\),此时我们将它接入\(A\)树,显然\(p\)的左子树都可以接入\(A\)树,我们接续递归右子树,判断接入哪棵树。
反之,我们同理可得: - \(val_p > k\),此时我们将它接入\(B\)树,显然\(p\)的右子树都可以接入\(B\)树,我们接续递归左子树,判断接入哪棵树。
code:
void split(int p,int k,int &x,int &y){//x,y是目前到的点
if(!p){x=y=0;return;}
if(val[p]<=k){x=p;split(rs[p],k,rs[p],y);}
else{y=p;split(ls[p],k,x,ls[p]);}
updata(p);//记得更新
}
再来看下\(merge\)操作。
我们按照随机的优先值维持平衡。
假设我们合并两个根节点为\(x\)和\(y\)的树。
且\(pfer_x\) \(>\) \(pfer_y\),那么\(x\)是爸爸,将\(x\)左子树不动,把\(y\)和\(x\)的右子树合并,再接入\(x\)的右子树。
反之亦然。
code:
int merge(int x,int y){
if(!x||!y)return x+y;//小技巧,同max(x,y),即返回不为0的点
if(pfer[x]>pfer[y]){
rs[x]=merge(rs[x],y);
updata(x);
return x;
}
else{
ls[y]=merge(x,ls[y]);
updata(y);
return y;
}
}
运用
学完这两个核心操作后,\(fhq-Treap\)已经学完一半了,其他的是它们的利用。
插入值\(k\)
先代入\(k-1\)将其分裂,新建点,然后合并
void ins(int k){
int x,y;
split(rt,k-1,x,y);
rt=merge(merge(x,New(k)),y);
}
删除值\(k\)
分裂成三棵子树\(A,B,C\),其中的值分别是\(<k\) , \(=k\) , \(>k\)
将\(B\)树左右儿子合并,然后合并三棵树。
void del(int k){
int x,y,z;
split(rt,k,x,z);
split(x,k-1,x,y);
if(y)y=merge(ls[y],rs[y]);
rt=merge(merge(x,y),z);
}
查询\(k\)的排名
直接按\(k-1\)分裂,返回\(x\)树大小
int rank(int k){
int x,y,ans;
split(rt,k-1,x,y);
ans=siz[x]+1;
rt=merge(x,y);
return ans;
}
查询第\(k\)大/小
和\(treap\)一样。
int kth(int k){
int p=rt;
while(true){
if(siz[ls[p]]+1==k)break;
else if(siz[ls[p]]+1>k)p=ls[p];
else k-=siz[ls[p]]+1,p=rs[p];
}
return val[p];
}
查询\(k\)值前驱或后继
前驱的话,依然是按\(k-1\)分裂,在\(x\)树一直往右边走,返回最后一个值。
后继相似,只是在\(y\)一直往左边走。
int pre_nxt(int k,int op){//前驱1,后继0
int x,y,p,ans;
split(rt,k,x,y);
k-=op;p=op?x:y;//小技巧
if(op)while(rs[p])p=rs[p];
else while(ls[p])p=ls[p];
ans=val[p];
rt=merge(x,y);
return ans;
}
查询值\(k\)是否存在
与删除类似
bool find(int k){
int x,y,z;bool ans;
split(rt,k,x,z);
split(x,k-1,x,y);
if(siz[y])ans=true;
else ans=false;
rt=merge(merge(x,y),z);
return ans;
}
模板题:普通平衡树
只写了\(90\)行不到,一般飞速就能码完吧。
交到\(luogu\),下面这份\(304ms\)。
快读快写+\(O2\)可以到\(200ms\)左右,好像还挺快的吧(?)
Code:
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int rt,T,cnt;
int val[N],siz[N],ls[N],rs[N],pfer[N];
void updata(int p){
if(!p)return;
siz[p]=siz[ls[p]]+siz[rs[p]]+1;
}
int New(int x){
int p=++cnt;
val[p]=x;siz[p]=1;
ls[p]=rs[p]=0;
pfer[p]=rand()*rand();
return p;
}
void split(int p,int k,int &x,int &y){
if(!p){x=y=0;return;}
if(val[p]<=k){x=p;split(rs[p],k,rs[p],y);}
else{y=p;split(ls[p],k,x,ls[p]);}
updata(p);
}
int merge(int x,int y){
if(!x||!y)return x+y;
if(pfer[x]>pfer[y]){
rs[x]=merge(rs[x],y);
updata(x);
return x;
}
else{
ls[y]=merge(x,ls[y]);
updata(y);
return y;
}
}
void ins(int k){
int x,y;
split(rt,k-1,x,y);
rt=merge(merge(x,New(k)),y);
}
void del(int k){
int x,y,z;
split(rt,k,x,z);
split(x,k-1,x,y);
if(y)y=merge(ls[y],rs[y]);
rt=merge(merge(x,y),z);
}
int Rank(int k){
int x,y,ans;
split(rt,k-1,x,y);
ans=siz[x]+1;
rt=merge(x,y);
return ans;
}
int kth(int k){
int p=rt;
while(true){
if(siz[ls[p]]+1==k)break;
else if(siz[ls[p]]+1>k)p=ls[p];
else k-=siz[ls[p]]+1,p=rs[p];
}
return val[p];
}
int pre_nxt(int k,int op){
int x,y,p,ans;
k-=op;
split(rt,k,x,y);
p=op?x:y;
if(op)while(rs[p])p=rs[p];
else while(ls[p])p=ls[p];
ans=val[p];
rt=merge(x,y);
return ans;
}
int main(){
srand(time(0));
scanf("%d",&T);
while(T--){
int op,x;
scanf("%d%d",&op,&x);
if(op==1)ins(x);
if(op==2)del(x);
if(op==3)printf("%d\n",Rank(x));
if(op==4)printf("%d\n",kth(x));
if(op==5||op==6)printf("%d\n",pre_nxt(x,op==5?1:0));
}
return 0;
}
文艺平衡树。
平衡树常规操作。
打旋转标记即可,类似于线段树,平衡树的中序遍历就是最后的答案。
code:
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int n,m,rt,cnt;
int val[N],pfer[N],ls[N],rs[N],siz[N],tag[N];
int New(int k){
int p=++cnt;
ls[p]=rs[p]=0;
siz[p]=1;val[p]=k;
pfer[p]=rand()*rand();
return p;
}
void updata(int p){
if(!p)return;
siz[p]=siz[ls[p]]+siz[rs[p]]+1;
}
void pushdown(int p){
if(!tag[p])return;
swap(ls[p],rs[p]);
tag[ls[p]]^=1;
tag[rs[p]]^=1;
tag[p]=0;
}
int merge(int x,int y){
if(!x||!y)return x+y;
if(pfer[x]>pfer[y]){
pushdown(x);
rs[x]=merge(rs[x],y);
updata(x);
return x;
}
else{
pushdown(y);
ls[y]=merge(x,ls[y]);
updata(y);
return y;
}
}
void SPLIT(int p,int sz,int &x,int &y){
if(!p){x=y=0;return;}
pushdown(p);
if(siz[ls[p]]+1<=sz){x=p;SPLIT(rs[p],sz-siz[ls[p]]-1,rs[p],y);}
else{y=p;SPLIT(ls[p],sz,x,ls[p]);}
updata(p);
}
void reverse(int l,int r){
int x,y,z;
SPLIT(rt,l-1,x,y);
SPLIT(y,r-l+1,y,z);
tag[y]^=1;
rt=merge(merge(x,y),z);
}
void write(int p){
if(!p)return;
pushdown(p);
write(ls[p]);
printf("%d ",val[p]);
write(rs[p]);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
rt=merge(rt,New(i));
while(m--){
int l,r;
scanf("%d%d",&l,&r);
reverse(l,r);
}
write(rt);
return 0;
}
郁闷的出纳员
打个\(tag\)标记,记录整体加减的工资,插入时记得\(ins(k-tag)\)。
用\(fhq-Treap\)做此题的删除比其他平衡树都简单。
只要代入最小工资\(split\),把较小的那课树舍弃即可。
code:
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
int rt,cnt,T,k;
int val[N],siz[N],ls[N],rs[N],pfer[N];
void updata(int p){
if(!p)return;
siz[p]=siz[ls[p]]+siz[rs[p]]+1;
}
int New(int x){
int p=++cnt;
val[p]=x;siz[p]=1;
ls[p]=rs[p]=0;
pfer[p]=rand()*rand();
return p;
}
void split(int p,int k,int &x,int &y){
if(!p){x=y=0;return;}
if(val[p]<=k) {x=p;split(rs[p],k,rs[p],y);}
else{y=p;split(ls[p],k,x,ls[p]);}
updata(p);
}
int merge(int x,int y){
if(!x||!y)return x+y;
if(pfer[x]>pfer[y]){
rs[x]=merge(rs[x],y);
updata(x);
return x;
}else{
ls[y]=merge(x,ls[y]);
updata(y);
return y;
}
}
void ins(int k){
int x,y;
split(rt,k-1,x,y);
rt=merge(merge(x,New(k)),y);
}
int kth(int k){
int p=rt;
while(true){
if(siz[ls[p]]+1==k)break;
else if(siz[ls[p]]+1>k)p=ls[p];
else k-=siz[ls[p]]+1,p=rs[p];
}
return val[p];
}
int Ans;
void Del(int F){
int x,y;
split(rt,F,x,y);
Ans+=siz[x];
rt=y;
}
int main(){
scanf("%d%d",&T,&k);
int tag=0;
while(T--){
char op[3];int t;
scanf("%s%d",op,&t);
if(op[0]=='I'&&t>=k)ins(t-tag);
else if(op[0]=='F')printf("%d\n",siz[rt]<t?-1:kth(siz[rt]-t+1)+tag);
else if(op[0]=='A')tag+=t;
else if(op[0]=='S')tag-=t,Del(k-tag-1);
}
printf("%d",Ans);
return 0;
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 解答了困扰我五年的技术问题
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· 用 C# 插值字符串处理器写一个 sscanf
· Java 中堆内存和栈内存上的数据分布和特点
· 开发中对象命名的一点思考
· DeepSeek 解答了困扰我五年的技术问题。时代确实变了!
· PPT革命!DeepSeek+Kimi=N小时工作5分钟完成?
· What?废柴, 还在本地部署DeepSeek吗?Are you kidding?
· DeepSeek企业级部署实战指南:从服务器选型到Dify私有化落地
· 程序员转型AI:行业分析