前面我们对平衡树有了个大概的了解

关于 Treap

Treap=Binary Search Tree + Heap

二叉搜索树 + 二叉堆(一般是小根堆)

Treap 每一个节点有两个值

一个值是平衡树的值,一个值是随机的(用于堆来保持平衡)

二叉堆的性质使其保持平衡

关于 FHQ Treap

这个名字来源很简单: FHQ 大佬发明的 Treap

普通的 Treap 是通过旋转来平衡的

而 FHQ Treap 是通过合并、分裂来维护的

核心操作

约定

变量名 作用
sz[] 存储以某个节点为根的子树的节点数(包括它自己)
l[] 某个节点的左孩子
r[] 某个节点的右孩子
fix[] 维护平衡的随机值
val[] 原本树上的值
cnt 节点个数
root 当前平衡树的根节点

基本操作

很简单,看一下就可以明白了

push_up

更新节点子树的节点数,注意包括节点本身

inline void push_up(int o){
sz[o]=sz[l[o]]+sz[r[o]]+1;
}
new_node

创建新节点,返回当前节点下标

别忘了初始化种子,不然随机数很多都一样

inline int new_node(int num){
cnt++;
sz[cnt]=1;
val[cnt]=num;
fix[cnt]=rand();
return cnt;
}

正式开始

这两个操作都是用递归实现的

常数有点大

不过应该还是很好理解的 QwQ

分裂 Split
void split(int now,int k,int &x,int &y){
if(!now) x=y=0;
else{
if(val[now]<=k) x=now,split(r[now],k,r[x],y);
else y=now,split(l[now],k,x,l[y]);
push_up(now);
}
}
void split(int now,int k,int &x,int &y){
if(!now) x=y=0;
else{
int tmp=size[l[now]]+1;
if(tmp<=k) x=now,split(r[now],k-tmp,r[x],y);
else y=now,split(l[now],k,x,l[y]);
push_up(now);
}
}

一个按 val 分裂,一个按 sz 分裂,思路基本相同。

作用:把以 now 为根的子树按照权值 k 分成 x 和 y 两颗子树

分完之后权值比 k 大的都在 y 子树中,其他的都在 x 子树中

思路(以按照权值分裂为例):

  1. 根节点为空,两个子树也为空,直接返回
  2. 根节点权值比 k 小,根节点和其左子树都给 x ,把根节点的右子树继续分成 r[x] 和 y 两颗子树
  3. 否则,根节点和其右子树都给 y ,把根节点的左子树继续分成 x 和 l[y] 两颗子树
  4. 分裂完毕,更新根节点的 sz
合并 Merge
int merge(int x,int y){
if(!x||!y) return x+y;
if(fix[x]<fix[y]) return r[x]=merge(r[x],y),push_up(x),x;
return l[y]=merge(x,l[y]),push_up(y),y;
}

作用:把 x 和 y 两颗子树合并成一颗树,并返回根节点

思路:

  1. 有一颗子树为空,直接返回 x + y (如果存在,不为空的那颗子树)
  2. fix[x]<fix[y] ,为满足小根堆的性质,先将 r[x] 和 y 合并,然后更新 x 的 sz ,最后将 x 作为根节点
  3. 否则,先将 l[y] 和 x 合并,然后更新 y 的 sz ,最后将 y 作为根节点

其他操作

有了这两个核心操作已经可以干很多事了

注意:分裂的子树 x 、 y 和 z 自行定义

Insert 插入节点

作用:添加值为 val 的节点

思路:把树按照插入的值分裂成 x 和 y ,再把插入的值看成一棵树与 y 合并,再将 y 和 x 合并

inline void ins(int val){
split(root,val,x,y);
root=merge(x,merge(new_node(val),y));
return;
}

还有另一种思路:直接将这颗子树与根节点合并,简单粗暴

void ins(int val){
root=merge(root,new_node(val));
}

Delete 删除节点

作用:删除值为 val 的节点

思路:找到以要删除的节点的值所在的子树,把其左子树和右子树合并,再把所有的子树合并,这个节点就删除了。

inline void del(int val){
split(root,val,x,z);split(x,val-1,x,y);
y=merge(l[y],r[y]);
root=merge(x,merge(y,z));
}

Rank 查询排名

作用:查询值为 val 再树中的排名。

思路:返回 val 左子树的 sz+1 ,记得合并

inline int rnk(int val){
split(root,val-1,x,y);
int ans=sz[x]+1;
root=merge(x,y);
return ans;
}

Kth 第 K 大

作用:查询以 o 为根节点的子树中排名为 rank 的值

思路:

  1. rank 等于当前节点左子树的 sz+1 ,直接返回 val[o]
  2. rank 小于左子树的 sz ,向左搜索, rank 不变
  3. 否则,向右搜索, rank 减去左子树的 sz+1
inline int kth(int o,int rank){
if(rank<=sz[l[o]]) return kth(l[o],rank);
if(rank==sz[l[o]]+1) return val[o];
return kth(r[o],rank-sz[l[o]]-1);
}

Precursor 前驱

前驱定义:小于 x 的最大值,不存在根据题目要求

作用:查询值为 v 的前驱

思路:把树按照 v-1 分裂,如果那棵树不为空,就查询排名为 sz 的(最大),否则按照要求

注意:按照 v-1 分裂使得一棵子树全部都是小于 v 的

inline int pre(int v){
split(root,v-1,x,y);
int ans=sz[x]?kth(x,sz[x]):-2147483647;
root=merge(x,y);
return ans;
}

Successor 后继

后继定义:大于 x 的最小值,不存在根据题目要求

作用:查询值为 v 的后继

思路:把树按照 v 分裂,如果那棵树不为空,就查询排名为 1 的(最小),否则按照要求

注意:按照 v 分裂使得一棵子树全部都是大于 v 的

inline int suc(int v){
split(root,v,x,y);
int ans=sz[y]?kth(y,1):2147483647;
root=merge(x,y);
return ans;
}

高级操作

垃圾回收

由于删除节点后那个下标就空出来,以后就都没用过了,所以可以用一个数组存起来,优化空间

假设垃圾桶是 bin[] ,尾指针是 top

这时我们的 Delete 应该改动一下,把没用的下标加入队列中

inline void del(int val){
split(root,val,x,z);split(x,val-1,x,y);
bin[++top]=y,y=merge(l[y],r[y]);
root=merge(x,merge(y,z));
}

增加新节点的函数也有所改动

inline int new_node(int num){
int cur=top?bin[top--]:++cnt;
l[cur]=r[cur]=0;
sz[cur]=1;
val[cur]=num;
fix[cur]=rand();
return cur;
}

注意:一定要把一个节点的左右儿子给清空,因为有可能之前用过

例题

洛谷 P3369

#include<bits/stdc++.h>
#define maxn 500001
using namespace std;
int sz[maxn],l[maxn],r[maxn],fix[maxn],val[maxn];
int T,cnt,n,m,x,y,z,p,a,root,bin[maxn],top;
inline int read(){
int x=0,f=1;char c=getchar();
for(;c<'0'||c>'9';c=getchar())
if(c=='-') f=-1;
for(;c>'/'&&c<':';c=getchar())
x=(x<<1)+(x<<3)+(c^48);
return x*f;
}
inline void write(int x){
if(x<0){x=-x;putchar('-');}
if(x>9) write(x/10);
putchar(x%10+'0');
}
inline void wl(int x){write(x);putchar('\n');}
inline void push_up(int x)
{
sz[x]=1+sz[l[x]]+sz[r[x]];
}
inline int new_node(int x){
int cur=top?bin[top--]:++cnt;
l[cur]=r[cur]=0;
sz[cur]=1;
val[cur]=x;
fix[cur]=rand();
return cur;
}
inline void split(int now,int k,int &x,int &y){
if(!now) x=y=0;
else{
if(val[now]<=k) x=now,split(r[now],k,r[x],y);
else y=now,split(l[now],k,x,l[y]);
push_up(now);
}
}
inline int merge(int x,int y){
if(!x||!y) return x+y;
if(fix[x]<fix[y]) return r[x]=merge(r[x],y),push_up(x),x;
return l[y]=merge(x,l[y]),push_up(y),y;
}
int kth(int now,int rank){
if(sz[l[now]]+1==rank) return val[now];
if(rank<=sz[l[now]]) return kth(l[now],rank);
return kth(r[now],rank-sz[l[now]]-1);
}
int main(){
srand((unsigned)time(NULL));
T=read();
while(T--){
p=read();a=read();
if(p==1){
split(root,a,x,y);
root=merge(merge(x,new_node(a)),y);
}
else if(p==2){
split(root,a,x,z);split(x,a-1,x,y);
bin[++top]=y;y=merge(l[y],r[y]);
root=merge(x,merge(y,z));
}
else if(p==3){
split(root,a-1,x,y);
wl(sz[x]+1);
root=merge(x,y);
}
else if(p==4) wl(kth(root,a));
else if(p==5){
split(root,a-1,x,y);
wl(kth(x,sz[x]));
root=merge(x,y);
}
else{
split(root,a,x,y);
wl(kth(y,1));
root=merge(x,y);
}
}
return 0;
}

可持久化

可持久化就是回到某一时刻进行操作,需要一个 rt[] 代替 root 存储每一时刻的根节点

我们只需将分裂劈出来的链上的每个点都 copy 一个新节点,合并的时候合并的也是新节点

这样合并时就不用新增节点了,因为合并之前肯定要分裂,而分裂已经新建好了,这样可以减少很多时间和空间

Copy 函数实现:

inline void copy(int cur,int old){
sz[cur]=sz[old];
val[cur]=val[old];
l[cur]=l[old];
r[cur]=r[old];
fix[cur]=fix[old];
return;
}

更改后的 Split :

void split(int now,int k,int &x,int &y){
if(!now) x=y=0;
else{
if(val[now]<=k) {
x=++cnt;
copy(x,now);
split(r[x],k,r[x],y);
push_up(x);
}
else{
y=++cnt;
copy(y,now);
split(l[y],k,x,l[y]);
push_up(y);
}
}
}

注意:可持久化的题输入会给你版本号 v ,记得 rt[i]=rt[v] ,并且所有操作在 rt[i] 上执行

例题

洛谷 P3835

#include<bits/stdc++.h>
#define N 500005*50
using namespace std;
inline int read(){
int x=0,f=1;char c=getchar();
for(;c<'0'||c>'9';c=getchar())
if(c=='-') f=-1;
for(;c>'/'&&c<':';c=getchar())
x=(x<<1)+(x<<3)+(c^48);
return x*f;
}
inline void write(int x){
if(x<0){x=-x;putchar('-');}
if(x>9) write(x/10);
putchar(x%10+'0');
}
inline void wl(int x){write(x);putchar('\n');}
int n,val[N],fix[N],l[N],r[N],sz[N],cnt,rt[500005];
inline int newnode(int x){
sz[++cnt]=1;
val[cnt]=x;
fix[cnt]=rand();
return cnt;
}
inline void copy(int cur,int old){
sz[cur]=sz[old];
val[cur]=val[old];
l[cur]=l[old];
r[cur]=r[old];
fix[cur]=fix[old];
return;
}
inline void push_up(int x){
sz[x]=sz[l[x]]+sz[r[x]]+1;
}
void split(int now,int k,int &x,int &y){
if(!now) x=y=0;
else{
if(val[now]<=k) {
x=++cnt;
copy(x,now);
split(r[x],k,r[x],y);
push_up(x);
}
else{
y=++cnt;
copy(y,now);
split(l[y],k,x,l[y]);
push_up(y);
}
}
}
int merge(int x,int y){
if(x==0||y==0) return x+y;
if(fix[x]<fix[y]){
r[x]=merge(r[x],y);
push_up(x);
return x;
}
else{
l[y]=merge(x,l[y]);
push_up(y);
return y;
}
}
int kth(int now,int rank){
if(sz[l[now]]+1==rank) return val[now];
if(rank<=sz[l[now]]) return kth(l[now],rank);
return kth(r[now],rank-sz[l[now]]-1);
}
int main(){
srand((unsigned)time(NULL));
n=read();
for(register int i=1;i<=n;i++){
int v,opt,a,b,x,y,z;
v=read(),opt=read(),a=read();
rt[i]=rt[v];
if(opt==1){
split(rt[i],a,x,y);
rt[i]=merge(merge(x,newnode(a)),y);
}
else if(opt==2){
split(rt[i],a,x,z);
split(x,a-1,x,y);
y=merge(l[y],r[y]);
rt[i]=merge(merge(x,y),z);
}
else if(opt==3){
split(rt[i],a-1,x,y);
wl(sz[x]+1);
rt[i]=merge(x,y);
}
else if(opt==4) wl(kth(rt[i],a));
else if(opt==5){
split(rt[i],a-1,x,y);
if(!sz[x]) puts("-2147483647");
else wl(kth(x,sz[x]));
rt[i]=merge(x,y);
}
else if(opt==6){
split(rt[i],a,x,y);
if(!sz[y]) puts("2147483647");
else wl(kth(y,1));
rt[i]=merge(x,y);
}
}
}

区间翻转

例题

洛谷 P3391

解析

首先要知道:这颗平衡树的中序遍历为原数组

并且运用了线段树下传懒标记的思想,用数组 tag[] 记录,表示当前子树是否要翻转

意思是:这个地方做个记号,以后到这了顺便传下去,这样减少了很多次重复的翻转

传标记 down 实现:

inline void down(int x){
if(tag[x]){
swap(l[x],r[x]);
if(l[x]) tag[l[x]]^=1;
if(r[x]) tag[r[x]]^=1;
tag[x]=0;
}
}

传标记中的 swap 相当于普通翻转中的交换,根节点即为中间点

要注意,这里需要按照 sz 分裂

只需把树分成了三个部分,将中间那个子树打上标记,再把所有的合并,就实现了区间翻转了

void rev(int l,int r){
split(root,l-1,x,y);
split(y,r-l+1,y,z);
tag[y]^=1;
y=merge(y,z);
root=merge(x,y);
}

更改后的分裂和合并

要先传标记

int merge(int x,int y){
if(!x||!y) return x+y;
if(fix[x]<fix[y]){
down(x);
r[x]=merge(r[x],y);
push_up(x);
return x;
}
else{
down(y);
l[y]=merge(x,l[y]);
push_up(y);
return y;
}
}
void split(int now,int k,int &x,int &y){
if(!now) x=y=0;
else{
down(now);
int tmp=size[l[now]]+1;
if(tmp<=k) x=now,split(r[now],k-tmp,r[x],y);
else y=now,split(l[now],k,x,l[y]);
push_up(now);
}
}

还需要一个输出,输出树的中序遍历

这里先传标记解决了标记没传完的问题

void print(int now){
if(!now) return;
down(now);
print(l[now]);
printf("%d ",val[now]);
print(r[now]);
}
代码
#include<bits/stdc++.h>
#define maxn 4000001
using namespace std;
int size[maxn],l[maxn],r[maxn],fix[maxn];
int val[maxn],tag[maxn];
int T,cnt,n,m,root,lt,rt;
inline int read(){
int x=0,f=1;char c=getchar();
for(;c<'0'||c>'9';c=getchar())
if(c=='-') f=-1;
for(;c>'/'&&c<':';c=getchar())
x=(x<<1)+(x<<3)+(c^48);
return x*f;
}
inline void write(int x){
if(x<0){x=-x;putchar('-');}
if(x>9) write(x/10);
putchar(x%10+'0');
}
inline void push_up(int x)
{
size[x]=1+size[l[x]]+size[r[x]];
}
inline void down(int x){
if(tag[x]){
swap(l[x],r[x]);
if(l[x]) tag[l[x]]^=1;
if(r[x]) tag[r[x]]^=1;
tag[x]=0;
}
}
inline int new_node(int x){
cnt++;
size[cnt]=1;
val[cnt]=x;
fix[cnt]=rand();
tag[cnt]=0;
return cnt;
}
int merge(int x,int y){
if(!x||!y) return x+y;
if(fix[x]<fix[y]){
down(x);
r[x]=merge(r[x],y);
push_up(x);
return x;
}
else{
down(y);
l[y]=merge(x,l[y]);
push_up(y);
return y;
}
}
void split(int now,int k,int &x,int &y){
if(!now) x=y=0;
else{
down(now);
int tmp=size[l[now]]+1;
if(tmp<=k) x=now,split(r[now],k-tmp,r[x],y);
else y=now,split(l[now],k,x,l[y]);
push_up(now);
}
}
void ins(int val){
root=merge(root,new_node(val));
}
void print(int now){
if(!now) return;
if(tag[now]) down(now);
print(l[now]);
write(val[now]),putchar(32);
print(r[now]);
}
void rev(int l,int r){
int x=0,y=0,z=0;
split(root,l-1,x,y);
split(y,r-l+1,y,z);
tag[y]^=1;
y=merge(y,z);
root=merge(x,y);
}
int main(){
srand(time(0));
n=read(),m=read();
for(register int i=1;i<=n;i++) ins(i);
while(m--){
lt=read(),rt=read();
rev(lt,rt);
}
print(root);
}


The End

c++ FHQ Treap的更多相关文章

  1. fhq treap最终模板

    新学习了fhq treap,厉害了 先贴个神犇的版, from memphis /* Treap[Merge,Split] by Memphis */ #include<cstdio> # ...

  2. NOI 2002 营业额统计 (splay or fhq treap)

    Description 营业额统计 Tiger最近被公司升任为营业部经理,他上任后接受公司交给的第一项任务便是统计并分析公司成立以来的营业情况. Tiger拿出了公司的账本,账本上记录了公司成立以来每 ...

  3. 【POJ2761】【fhq treap】A Simple Problem with Integers

    Description You have N integers, A1, A2, ... , AN. You need to deal with two kinds of operations. On ...

  4. 【fhq Treap】bzoj1500(听说此题多码上几遍就能不惧任何平衡树题)

    1500: [NOI2005]维修数列 Time Limit: 10 Sec  Memory Limit: 64 MBSubmit: 15112  Solved: 4996[Submit][Statu ...

  5. 「FHQ Treap」学习笔记

    话说天下大事,就像fhq treap —— 分久必合,合久必分 简单讲一讲.非旋treap主要依靠分裂和合并来实现操作.(递归,不维护fa不维护cnt) 合并的前提是两棵树的权值满足一边的最大的比另一 ...

  6. FHQ Treap摘要

    原理 以随机数维护平衡,使树高期望为logn级别 不依靠旋转,只有两个核心操作merge(合并)和split(拆分) 因此可持久化 先介绍变量 ; int n; struct Node { int v ...

  7. FHQ Treap小结(神级数据结构!)

    首先说一下, 这个东西可以搞一切bst,treap,splay所能搞的东西 pre 今天心血来潮, 想搞一搞平衡树, 先百度了一下平衡树,发现正宗的平衡树写法应该是在二叉查找树的基础上加什么左左左右右 ...

  8. 在平衡树的海洋中畅游(四)——FHQ Treap

    Preface 关于那些比较基础的平衡树我想我之前已经介绍的已经挺多了. 但是像Treap,Splay这样的旋转平衡树码亮太大,而像替罪羊树这样的重量平衡树却没有什么实际意义. 然而类似于SBT,AV ...

  9. 浅谈fhq treap

    一.简介 fhq treap 与一般的treap主要有3点不同 1.不用旋转 2.以merge和split为核心操作,通过它们的组合实现平衡树的所有操作 3.可以可持久化 二.核心操作 代码中val表 ...

  10. fhq treap 学习笔记

    序 今天心血来潮,来学习一下fhq treap(其实原因是本校有个OIer名叫fh,当然不是我) 简介 fhq treap 学名好像是"非旋转式treap及可持久化"...听上去怪 ...

随机推荐

  1. 数组 indexOf()

    众所周知,indexOf()这个方法经常出现在字符串的使用中,也许是用来寻找字符串中某一字符在字符串中的位置,或者也可以用来寻找字符串中重复出现的字符有哪些.对于刚接触 JS 的我们来说,在对数组的操 ...

  2. Mybatis-typeAliases的作用

    其他具体代码请访问->mybatis的快速入门 1.在mybatis核心配置文件SqlMapperConfig.xml中配置别名 <!-- 定义别名--> <typeAlias ...

  3. LC-54

    给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素. 示例 1: 输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] 输出:[1,2 ...

  4. Python-初见

    目录 概述 关键字 标准数据类型 Number String List Tuple Set Dictionary 删除对象 数据类型转换 推导式 运算符 迭代器与生成器 迭代器 生成器 函数 参数传递 ...

  5. drf路由组件(4星)

    路由组件(4星) 路由Routers 对于视图集ViewSet, 我们除了可以自己手动指明请求方式与动作action之间的对应关系外,还可以使用Routers来帮助我们快速实现路由信息. REST f ...

  6. linux修改静态ip

    1.修改配置文件 vi /etc/sysconfig/network-scripts/ifcfg-ens32 bootproto:设置为静态 onboot:开机自启 ipaddr:ip地址 netma ...

  7. asp.net core + jenkins 实现自动化发布

    由于部署个人博客系统的服务器只有2G内存,每次利用jenkins编译,发布的时候jenkins老是挂,因此新买了一台轻量应用服务器,专门用于个人博客系统的持续发布任务,下面讲解如何利用jenkins实 ...

  8. Python 中的鸭子类型和猴子补丁

    原文链接: Python 中的鸭子类型和猴子补丁 大家好,我是老王. Python 开发者可能都听说过鸭子类型和猴子补丁这两个词,即使没听过,也大概率写过相关的代码,只不过并不了解其背后的技术要点是这 ...

  9. border 流光高光

    <template> <div>   <div class="conic"></div> <div class="c ...

  10. 2021.11.30 eleveni的水省选题的记录

    2021.11.30 eleveni的水省选题的记录 因为eleveni比较菜,eleveni决定先刷图论,再刷数据结构,同时每天都要刷dp.当然,对于擅长的图论,eleveni决定从蓝题开始刷.当然 ...