「笔记」FHQ-Treap
一、简介
Treap=Tree+Heap。Treap 是利用堆的性质来维护平衡的一种平衡树。
对每个节点额外存储一个随机值 \(rnd\)(作为关键值),根据随机值调整 Treap 的形态,使其满足 BST 性质外,还使其随机值满足堆的性质(大根堆或小根堆)。
关键值随机生成,则 Treap 树高期望 \(\mathcal O(\log n)\)。
无旋 Treap 以分裂和合并为核心。其操作方式使它天生支持维护序列、可持久化等特性。
考场上建议写 FHQ-Treap。
二、核心操作
1. 分裂
按权值分割:根据一个值 \(k\) 将一棵树分裂为两棵,第一棵树中的权值都 \(\leq k\),第二棵树中的权值都 \(>k\)。
设权值较小的树为左树,另一棵为右树。
根据二叉搜索树左小右大的性质,可确定分裂的过程:
- 比较根节点权值 与 给定权值的大小关系
- 若小于给定权值,则根及左子树点都 \(\leq\) 给定权值,将其归入左树中。然后向右子树递归,继续分裂子树。
- 否则,根及右子树点都 \(\geq\) 给定权值,将其归入右树中。然后向左子树递归,继续分裂子树。
- 递归至叶节点后退出。
void split(int p,int &x,int &y,int k){
if(!p){x=y=0;return ;}
if(val[p]<=k) x=p,split(rc[p],rc[p],y,k);
else y=p,split(lc[p],x,lc[p],k);
upd(p);
}
按大小分割:第一棵树的大小为 \(k\),第二棵树为剩下的部分。
void split2(int p,int &x,int &y,int k){
if(!p){x=y=0;return ;}
if(sz[lc[p]]+1<=k) x=p,split(rc[p],rc[p],y,k-sz[lc[p]]-1);
else y=p,split2(lc[p],x,lc[p],k);
upd(p);
}
显然单次分裂复杂度是 \(\mathcal O(\text{height})\) 的。期望树高 \(\log n\),单次分裂期望复杂度为 \(\mathcal O(\log n)\)。
2. 合并
\(\text{merge}(x,y)\) 将以 \(x,y\) 节点为根的两棵树合并为一棵,并返回新树根的编号。
注意:合并的两棵树 一定是 Split 分裂获得的两棵树!
因此,合并的前提是,\(x\) 的权值全部 \(<\) \(y\) 的权值
经过 Split 后,得到的两棵 Treap 是有序的,左树任一点权值 必然小于右树任一点。
只需要考虑按关键值,确定父子关系即可(这里关键值满足 小根堆 的性质)。假设两个根为 \(l,r\):
- 若 \(rnd(l)<rnd(r)\),那么将 \(l\) 作为新树根,保留 \(l\) 的左子树,然后将 \(l\) 的右儿子和 \(r\) 合并得到的新树作为右子树。
- 否则,将 \(r\) 作为新树根,保留 \(r\) 的右子树,然后将 \(l\) 与 \(r\) 的左儿子合并得到的新树作为左子树。
- 否则,将右树根作为新树根,新树继承 右树的右子树。
递归,将 左树 与 右树的左子树 合并,作为新树的左子树。 - 当左 / 右子树不存在时退出。
int merge(int x,int y){ //前提:x 的权值全部 < y 的权值
if(!x||!y) return x+y;
if(rnd[x]<rnd[y]){rc[x]=merge(rc[x],y),upd(x);return x;}
else lc[y]=merge(x,lc[y]),upd(y);
return y;
}
三、其他操作
剩下的所有操作都以分裂、合并为基础即可。
例如插入就是先把树分裂成两半,然后把这两半和新节点分别合并。
1. 基本操作
\(\text{upd}(x)\) 更新以 \(x\) 为根的子树大小(或其他信息)。
\(\text{getnew}(k)\) 新建一权值为 \(k\) 的节点,并返回其编号。
void upd(int x){
sz[x]=sz[lc[x]]+sz[rc[x]]+1;
}
int getnew(int k){
val[++tot]=k,rnd[tot]=rand(),sz[tot]=1;
return tot;
}
2. 插入操作
\(\text{insert}(k)\) 新建一个权值为 \(k\) 的节点,并将其插入至 Treap 中。
先 \(\text{split}(k)\),得到左树 \(x\)(左树点权值 \(\leq k\))和右树 \(y\)(右树点权值 \(>k\))。
新建一权值为 \(k\) 的节点,然后把左树 \(x\)、\(k\) 节点、右树 \(y\) 三部分 \(\text{merge}\) 起来。
void insert(int k){
int x=0,y=0;
split(rt,x,y,k),rt=merge(merge(x,getnew(k)),y);
}
3. 删除操作
\(\text{erase}(k)\) 删除 Treap 中一个权值为 \(k\) 的节点。
考虑一棵树上可能有重复权值的节点,我们需要得到小于、等于、大于 \(k\) 三棵树,然后删除等于\(k\) 的那棵树的根节点(可以通过合并两个儿子实现删根),最后重新合并起来。
void erase(int k){
int x=0,y=0,z=0;
split(rt,x,z,k),split(x,x,y,k-1);
rt=merge(merge(x,merge(lc[y],rc[y])),z);
}
4. 查询排名
\(\text{rank}(k)\) 查询权值 \(k\) 的排名。
将 \(1\) 到 \(k-1\) 的元素提出来得到树 \(x\),那么 \(k\) 的排名就是 \(x\) 的大小加 \(1\)。
int rank(int k){
int x=0,y=0,ans;
split(rt,x,y,k-1),ans=sz[x]+1,rt=merge(x,y);
return ans;
}
5. 第 k 小数
\(\text{kth}(rt,k)\) 查询排名 \(k\) 在子树 \(rt\) 中的权值,返回对应节点的编号。
提供两种方法:
-
把前 \(k-1\) 个数拿出来,再从剩下的树种找第 \(1\) 个元素就是答案。使用 \(\text{split2}\)(按大小分割)可以实现。
int kth(int rt,int k){ int x=0,y=0,z=0,ans; split2(rt,x,y,k-1),split2(y,1,y,z); ans=y,rt=merge(merge(x,y),z); return ans; }
-
递归。根据二叉树搜索树左小右大的性质 和 维护的子树大小可得答案。
int kth(int rt,int k){ if(!rt) return 0; if(sz[lc[rt]]+1==k) return rt; if(sz[lc[rt]]+1>k) return kth(lc[rt],k); return kth(rc[rt],k-sz[lc[rt]]-1); }
6. 查询前驱/后继
查询前驱:
- \(\text{pre}(k)\) 查询 \(k\) 的前驱。前驱定义为小于 \(k\) 且最大的数。
- 先得到 \(1\) 到 \(k-1\) 构成的数,用 \(\text{kth}\) 函数求出其中最后一个元素即可。
int pre(int k){
int x=0,y=0,ans;
split(rt,x,y,k-1),ans=kth(x,sz[x]),rt=merge(x,y);
return ans;
}
查询后继:
- \(\text{nxt}(k)\) 查询 \(k\) 的后继。后继定义为大于 \(k\) 且最小的数。
- 先得到 \(1\) 到 \(k\) 构成的树,用 \(\text{kth}\) 函数求出剩下的树种的最后一个即可。
int nxt(int k){
int x=0,y=0,ans;
split(rt,x,y,k),ans=kth(y,1),rt=merge(x,y);
return ans;
}
四、模板
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,op,x;
struct Treap{
int rt,tot,lc[N],rc[N],val[N],sz[N],rnd[N];
void upd(int x){
sz[x]=sz[lc[x]]+sz[rc[x]]+1;
}
int getnew(int k){
val[++tot]=k,rnd[tot]=rand(),sz[tot]=1;
return tot;
}
void split(int p,int &x,int &y,int k){
if(!p){x=y=0;return ;}
if(val[p]<=k) x=p,split(rc[p],rc[p],y,k);
else y=p,split(lc[p],x,lc[p],k);
upd(p);
}
int merge(int x,int y){ //前提:x 的权值全部 < y 的权值
if(!x||!y) return x+y;
if(rnd[x]<rnd[y]){rc[x]=merge(rc[x],y),upd(x);return x;}
else lc[y]=merge(x,lc[y]),upd(y);
return y;
}
void insert(int k){
int x=0,y=0;
split(rt,x,y,k),rt=merge(merge(x,getnew(k)),y);
}
void erase(int k){
int x=0,y=0,z=0;
split(rt,x,z,k),split(x,x,y,k-1);
rt=merge(merge(x,merge(lc[y],rc[y])),z);
}
int rank(int k){
int x=0,y=0,ans;
split(rt,x,y,k-1),ans=sz[x]+1,rt=merge(x,y);
return ans;
}
int kth(int rt,int k){
if(!rt) return 0;
if(sz[lc[rt]]+1==k) return rt;
if(sz[lc[rt]]+1>k) return kth(lc[rt],k);
return kth(rc[rt],k-sz[lc[rt]]-1);
}
int pre(int k){
int x=0,y=0,ans;
split(rt,x,y,k-1),ans=kth(x,sz[x]),rt=merge(x,y);
return ans;
}
int nxt(int k){
int x=0,y=0,ans;
split(rt,x,y,k),ans=kth(y,1),rt=merge(x,y);
return ans;
}
}T;
signed main(){
srand(time(0));
scanf("%d",&n);
while(n--){
scanf("%d%d",&op,&x);
if(op==1) T.insert(x);
else if(op==2) T.erase(x);
else if(op==3) printf("%d\n",T.rank(x));
else if(op==4) printf("%d\n",T.val[T.kth(T.rt,x)]);
else if(op==5) printf("%d\n",T.val[T.pre(x)]);
else printf("%d\n",T.val[T.nxt(x)]);
}
return 0;
}
五、区间操作
区间修改只需分裂出这个区间,然后打个标记合并即可。举个栗子:
维护一个序列,支持区间翻转,动态查询每个位置上的数字。
查询区间 \([l,r]\),那么将整个序列分裂为 \([1,l-1]\)、\([l,r]\)、\([r+1,n]\)(按大小分裂),然后再在 \([l,r]\) 打一个 reverse 区间标记即可。
最后将三个区间合并。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,l,r;
struct Treap{
int rt,tot,lc[N],rc[N],val[N],sz[N],rnd[N],rev[N];
void pushup(int x){
sz[x]=sz[lc[x]]+sz[rc[x]]+1;
}
void pushdown(int x){
if(!rev[x]) return ;
swap(lc[x],rc[x]),rev[lc[x]]^=1,rev[rc[x]]^=1;
rev[x]=0;
}
int getnew(int k){
val[++tot]=k,rnd[tot]=rand(),sz[tot]=1;
return tot;
}
void split(int p,int &x,int &y,int k){ //按子树大小分裂
if(!p){x=y=0;return ;}
pushdown(p);
if(sz[lc[p]]+1<=k) x=p,split(rc[p],rc[p],y,k-sz[lc[p]]-1);
else y=p,split(lc[p],x,lc[p],k);
pushup(p);
}
int merge(int x,int y){
if(!x||!y) return x+y;
if(rnd[x]<rnd[y]){
pushdown(x),rc[x]=merge(rc[x],y),pushup(x);
return x;
}
else pushdown(y),lc[y]=merge(x,lc[y]),pushup(y);
return y;
}
}T;
void print(int x){
T.pushdown(x);
if(T.lc[x]) print(T.lc[x]);
printf("%d ",T.val[x]);
if(T.rc[x]) print(T.rc[x]);
}
signed main(){
srand(time(0));
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
T.rt=T.merge(T.rt,T.getnew(i));
while(m--){
scanf("%d%d",&l,&r);
int x=0,y=0,z=0;
T.split(T.rt,x,y,l-1),T.split(y,y,z,r-l+1);
T.rev[y]^=1,T.rt=T.merge(x,T.merge(y,z));
}
print(T.rt);
return 0;
}
原文地址:https://www.cnblogs.com/mytqwqq/p/15057231.html
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 基于python和flask实现http接口过程解析
- Python xpath表达式如何实现数据处理
- Python脚本破解压缩文件口令实例教程(zipfile)
- 使用keras实现Precise, Recall, F1-socre方式
- Python Django搭建网站流程图解
- Pandas对DataFrame单列/多列进行运算(map, apply, transform, agg)
- keras自定义损失函数并且模型加载的写法介绍
- pandas DataFrame运算的实现
- Python流程控制语句的深入讲解
- 在keras里面实现计算f1-score的代码
- Keras官方中文文档:性能评估Metrices详解
- Django QuerySet查询集原理及代码实例
- Python中zipfile压缩文件模块的基本使用教程
- 基于nexus3配置Python仓库过程详解
- Python Django中间件使用原理及流程分析