FHQ-Treap学习笔记
平衡树与FHQ-Treap
平衡树(即平衡二叉搜索树),是通过一系列玄学操作让二叉搜索树(BST)处于较平衡的状态,防止在某些数据下退化(BST在插入值单调时,树形不平衡,单次会退化成 \(\mathcal{O}(n)\) )。常见的平衡树有Treap、FHQ-Treap、Splay、AVL、红黑树等。平衡树应用广泛,可用于维护集合、数列,辅助LCT等。
FHQ-Treap是一种常数较小,码量较小,功能较多的平衡树。它通过分裂与合并操作,且在合并时根据节点的随机权值确定合并方案,来维持平衡。单次复杂度 \(\mathcal{O}(\log n)\)。
节点信息&基本操作
int s[N];//s[i]以i为根的子树大小
int r[N];//r[i]节点i的随机权值
int v[N];//v[i]节点i的权值
int ch[N][2];//ch[i][0]节点i的左儿子,ch[i][1]节点i的右儿子
int siz;//使用过的节点树,注意并不是树的大小
int rt;//根节点编号
int New(int val) {s[++siz] = 1; r[siz] = rand(); v[siz] = val; return siz;}//创建新节点,返回它的编号
void upd(int p) {s[p] = s[ch[p][0]] + s[ch[p][1]] + 1;}//更新子树大小
合并&分裂
merge(x, y)将以x,y为根的两棵树合并为一棵,并返回合并后的根节点编号。以x为根的树中所有任意的权值不大于以y为根的树中所有任意的权值。
我们可以用递归的方式来实现。为了达到平衡,我们需要利用随机权值。通过比较x和y节点的随机权值来确定合并方案。
- x, y两棵树都非空时:

- x,y都为空时:直接返回0。
- x,y中一空一非空时:返回非空树的根节点,用位运算中的按位或实现。
注意以x为根的树中所有任意的权值时刻不大于以y为根的树中所有任意的权值。
int merge(int x, int y) {
if(!x || !y) return x | y;
return r[x] < r[y] ? (ch[x][1] = merge(ch[x][1], y), upd(x), x) : (ch[y][0] = merge(x, ch[y][0]), upd(y), y);
}
split(p, val, x, y)将以p为根的树分裂为两棵树,根分别是x和y。其中x树中所有节点的权值小于等于val,y树中所有节点的权值大于val。
如果节点p的权值 \(\le val\),那么p和p的左子树的权值都 \(\le val\),它们一定属于x树。把p节点和p的左子树一并给x,然后在p的右子树内继续分裂。
反之,如果节点p的权值 \(>val\),那么p和p的右子树的权值都 \(>val\),它们一定属于y树。把p节点和p的右子树一并给y,然后在p的左子树内继续分裂。
void split(int p, int val, int &x, int &y) {
if(!p) {x = y = 0; return;}
v[p] <= val ? (x = p, split(ch[p][1], val, ch[p][1], y)) :
(y = p, split(ch[p][0], val, x, ch[p][0]));
upd(p);
}
split(p, sss, x, y)将以p为根的树分裂为两棵树,根分别是x和y。其中x树为p树中序遍历的前sss个节点组成的树(按大小分裂)。
与按权值分裂的思想类似。当往右子树走时,要先减去左子树和根节点的大小(左子树大小+1),因为这个大小是相对以当前节点为根的子树而言的。
void split(int p, int sss, int &x, int &y) {
if(!p) {x = y = 0; return;}
if(sss > s[ch[p][0]]) x = p, split(ch[p][1], sss - s[ch[p][0]] - 1, ch[p][1], y);
else y = p, split(ch[p][0], sss, x, ch[p][0]);
upd(p);
}
插入
设插入的数为 \(val\) ,先按 \(val-1\)分裂出x和y,然后新建权值为 \(val\) 的节点p。依次合并x,p,y即可。
void insert(int val) {
int x, y;
split(rt, val - 1, x, y);
rt = merge(merge(x, New(val)), y);
}
在给定位置插入:把按权值分裂改成按大小分裂就行。
in void ins(int l, char val) {//在l位置后插入值为val的节点
int x, y;
split(rt, l, x, y);
rt = merge(merge(x, New(val)), y);
}
删除
设删除的数为 \(val\) ,先按 \(val\) 分裂出y和z,再将y按 \(val-1\) 分裂出x和y。如果y不为空,说明有值为 \(val\) 的数。如果只删除一个,那么将y设为y的左右儿子合并的结果,再依次合并x,y,z即可;如果删除所有值为 \(val\) 的数,直接合并x和z即可。
void erase(int val) {
int x, y, z;
split(rt, val, x, z);
split(x, val - 1, x, y);
if(y) y = merge(ch[y][0], ch[y][1]);
rt = merge(merge(x, y), z);
}
/*
void erase_all(int val) {
int x, y, z;
split(rt, val, x, z);
split(x, val - 1, x, y);
rt = merge(x, z);
}
*/
区间删除:分裂出目标树,合并其它树就得到了删除后的树。
void eraselr(int l, int r) {
int x, y, z;
split(rt, r, y, z);
split(y, l - 1, x, y);
rt = merge(x, z);
}
查找值的排名
排名定义为比它小的数的个数加1。分裂出权值比它小的树,答案就是这棵树的大小+1。
int rank(int val) {
int x, y, ans;
split(rt, val - 1, x, y);
ans = s[x] + 1;
rt = merge(x, y);
return ans;
}
根据排名查找值
从根节点出发,根据子树大小判断目标是否为当前节点,或要往哪里走。当往右子树走时,要先减去左子树和根节点的大小(左子树大小+1),因为这个排名是相对以当前节点为根的子树而言的。
int val(int rk) {
int p = rt;
while(p) {
if(s[ch[p][0]] + 1 == rk) return v[p];
else if(rk <= s[ch[p][0]]) p = ch[p][0];
else rk = rk - s[ch[p][0]] - 1, p = ch[p][1];
}
}
前驱&后继
前驱:小于 \(x\),且最大的数
后继:大于 \(x\),且最小的数
顺着这个思路,我们只要分裂出满足前半句话的树,然后就很容易在这棵树内得到满足后半句话的节点。
int prev(int val) {
int x, y, tmp;
split(rt, val - 1, x, y);
tmp = x;
while(ch[tmp][1]) tmp = ch[tmp][1];
rt = merge(x, y);
return v[tmp];
}
int next(int val) {
int x, y, tmp;
split(rt, val, x, y);
tmp = y;
while(ch[tmp][0]) tmp = ch[tmp][0];
rt = merge(x, y);
return v[tmp];
}
维护区间
FHQ-Treap也可以用于维护区间。如果我们让节点在真正序列中的位置满足BST性质,那么真正序列就是树的中序遍历。一般地,FHQ-Treap进行一次区间修改/查询的方法是:分裂出目标树 -> 修改/查询(一般使用类似线段树的懒标记) -> 合并。这里就要按大小分裂了。
与线段树不同的是,FHQ-Treap支持任意位置插入/删除、区间翻转等操作,但常数较大。
对于每个操作 \([l,r]\),我们先按大小分裂出这棵目标树。方法是,先分裂出 \(y=[1,r]\) 和 \(z=[r+1,n]\),再将y分裂出 \(x=[1,l-1]\) 和 \(y=[l,r]\)。此时的y就是目标树了。
然后,我们给y打上懒标记。注意分裂/合并时要下传标记。下传标记的方法视情况而定。
例题
所有操作前文已讲。
#include <cstdio>
#include <cstring>
#include <ctime>
#include <cstdlib>
#include <algorithm>
using namespace std;
#define in __inline__
#define rei register int
char inputbuf[1 << 23], *p1 = inputbuf, *p2 = inputbuf;
#define getchar() (p1 == p2 && (p2 = (p1 = inputbuf) + fread(inputbuf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
in int read() {
register int res = 0;
char ch = getchar();
bool f = true;
for(; ch < '0' || ch > '9'; ch = getchar())
if(ch == '-') f = false;
for(; ch >= '0' && ch <= '9'; ch = getchar())
res = res * 10 + (ch ^ 48);
return f ? res : -res;
}
in void write(register int x){
static unsigned char _q[35]; register unsigned char t=0;
for(; x; x /= 10) _q[++t] = x % 10;
for(; t; --t) putchar(_q[t] + 48);
putchar(32);
}
in void swap(int &x, int &y) {x ^= y ^= x ^= y;}
const int N = 1e5 + 5;
int ch[N][2], s[N], r[N], v[N], siz, rt;
int New(int val) {s[++siz] = 1; r[siz] = rand(); v[siz] = val; return siz;}
void upd(int p) {s[p] = s[ch[p][0]] + s[ch[p][1]] + 1;}
int merge(int x, int y) {
if(!x || !y) return x | y;
if(r[x] > r[y]) {ch[x][1] = merge(ch[x][1], y); upd(x); return x;}
else {ch[y][0] = merge(x, ch[y][0]); upd(y); return y;}
}
void split(int p, int val, int &x, int &y) {
if(!p) {x = y = 0; return;}
v[p] <= val ? (x = p, split(ch[p][1], val, ch[p][1], y)) :
(y = p, split(ch[p][0], val, x, ch[p][0]));
upd(p);
}
in void insert(int val) {
int x, y;
split(rt, val - 1, x, y);
rt = merge(merge(x, New(val)), y);
}
in void erase(int val) {
int x, y, z;
split(rt, val, x, z);
split(x, val - 1, x, y);
if(y) y = merge(ch[y][0], ch[y][1]);
rt = merge(merge(x, y), z);
}
in int rank(int val) {
int x, y, ans;
split(rt, val - 1, x, y);
ans = s[x] + 1;
rt = merge(x, y);
return ans;
}
in int val(int rk) {
int p = rt;
while(p) {
if(s[ch[p][0]] + 1 == rk) return v[p];
else if(rk <= s[ch[p][0]]) p = ch[p][0];
else rk = rk - s[ch[p][0]] - 1, p = ch[p][1];
}
}
in int prev(int val) {
int x, y, tmp;
split(rt, val - 1, x, y);
tmp = x;
while(ch[tmp][1]) tmp = ch[tmp][1];
rt = merge(x, y);
return v[tmp];
}
in int next(int val) {
int x, y, tmp;
split(rt, val, x, y);
tmp = y;
while(ch[tmp][0]) tmp = ch[tmp][0];
rt = merge(x, y);
return v[tmp];
}
int main() {
int q = read(), opt, x, lst = 0, ans = 0;
srand(time(0));
for(; q; --q) {
opt = read(), x = read();
if(opt == 1) insert(x);
else if(opt == 2) erase(x);
else if(opt == 3) write(rank(x)));
else if(opt == 4) write(val(x));
else if(opt == 5) write(prev(x));
else write(next(x));
}
}
我们显然可以得到一个性质:同一个区间翻转偶数次等同于没有翻转。 于是我们可以利用异或运算来实现(\(0\operatorname{xor}\) 奇数次 \(1\) 得到 \(1\),偶数次得到 \(0\))。
暴力翻转整棵目标树就是交换每个节点的左右儿子,那么下传标记时更新左右儿子的标记,并交换左右儿子即可。
最终答案就是整棵树中序遍历得到的序列。递归输出时也要下传标记。
const int N = 1e5 + 5;
int s[N], ch[N][2], r[N], v[N], siz, rt, n, m, t[N];
in void push(int p) {
if(!t[p]) return;
swap(ch[p][0], ch[p][1]);
if(ch[p][0]) t[ch[p][0]] ^= 1;
if(ch[p][1]) t[ch[p][1]] ^= 1;
t[p] = 0;
}
in int New(int val) {
v[++siz] = val; s[siz] = 1; r[siz] = rand(); return siz;
}
in void upd(int p) {s[p] = s[ch[p][0]] + s[ch[p][1]] + 1;}
void print(int p) {
if(!p) return;
push(p);
print(ch[p][0]);
write(v[p]);
print(ch[p][1]);
}
int merge(int x, int y) {
if(!x || !y) return x | y;
return r[x] < r[y] ? (push(x), ch[x][1] = merge(ch[x][1], y), upd(x), x) : (push(y), ch[y][0] = merge(x, ch[y][0]), upd(y), y);
}
void split(int p, int sss, int &x, int &y) {
if(!p) {x = y = 0; return;}
push(p);
if(sss > s[ch[p][0]]) x = p, split(ch[p][1], sss - s[ch[p][0]] - 1, ch[p][1], y);
else y = p, split(ch[p][0], sss, x, ch[p][0]);
upd(p);
}
void rotate() {
int l = read(), r = read(), x, y, z;
split(rt, r, y, z);
split(y, l - 1, x, y);
t[y] ^= 1;
rt = merge(merge(x, y), z);
}
int main() {
srand(time(0));
n = read(); m = read();
for(rei i = 1; i <= n; ++i) rt = merge(rt, New(i));
for(; m; --m) rotate();
print(rt);
return 0;
}
有什么问题珂以在评论区提出并吊打这个蒟蒻/kk
FHQ-Treap学习笔记的更多相关文章
- fhq treap 学习笔记
序 今天心血来潮,来学习一下fhq treap(其实原因是本校有个OIer名叫fh,当然不是我) 简介 fhq treap 学名好像是"非旋转式treap及可持久化"...听上去怪 ...
- FHQ treap学习(复习)笔记
.....好吧....最后一篇学习笔记的flag它倒了..... 好吧,这篇笔记也鸽了好久好久了... 比赛前刷模板,才想着还是补个坑吧... FHQ,这个神仙(范浩强大佬),发明了这个神仙的数据结构 ...
- 左偏树 / 非旋转treap学习笔记
背景 非旋转treap真的好久没有用过了... 左偏树由于之前学的时候没有写学习笔记, 学得也并不牢固. 所以打算写这么一篇学习笔记, 讲讲左偏树和非旋转treap. 左偏树 定义 左偏树(Lefti ...
- treap学习笔记
treap是个很神奇的数据结构. 给你一个问题,你可以解决它吗? 这个问题需要treap这个数据结构. 众所周知,二叉查找树的查找效率低的原因是不平衡,而我们又不希望用各种奇奇怪怪的旋转来使它平衡,那 ...
- fhq treap抄袭笔记
目录 碎碎念 点一下 注意!!! 模板 fhq treap 碎碎念 我咋感觉合并这么像左偏树呢 ps:难道你们的treap都是小头堆的吗 fhq真的是神人 现在看以前学的splay是有点恶心,尤其是压 ...
- Treap + 无旋转Treap 学习笔记
普通的Treap模板 今天自己实现成功 /* * @Author: chenkexing * @Date: 2019-08-02 20:30:39 * @Last Modified by: chenk ...
- [Treap][学习笔记]
平衡树 平衡树就是一种可以在log的时间复杂度内完成数据的插入,删除,查找第k大,查询排名,查询前驱后继以及其他许多操作的数据结构. Treap treap是一种比较好写,常数比较小,可以实现平衡树基 ...
- Treap-平衡树学习笔记
平衡树-Treap学习笔记 最近刚学了Treap 发现这种数据结构真的是--妙啊妙啊~~ 咳咳.... 所以发一发博客,也是为了加深蒟蒻自己的理解 顺便帮助一下各位小伙伴们 切入正题 Treap的结构 ...
- 「FHQ Treap」学习笔记
话说天下大事,就像fhq treap —— 分久必合,合久必分 简单讲一讲.非旋treap主要依靠分裂和合并来实现操作.(递归,不维护fa不维护cnt) 合并的前提是两棵树的权值满足一边的最大的比另一 ...
- 「学习笔记」 FHQ Treap
FHQ Treap FHQ Treap (%%%发明者范浩强年年NOI金牌)是一种神奇的数据结构,也叫非旋Treap,它不像Treap zig zag搞不清楚(所以叫非旋嘛),也不像Splay完全看不 ...
随机推荐
- C. Journey bfs 拓扑排序+dp
C. Journey 补今天早训 这个是一个dp,开始我以为是一个图论,然后就写了一个dij和网络流,然后mle了,不过我觉得如果空间开的足够的,应该也是可以过的. 然后看了题解说是一个dp,这个dp ...
- N - Subpalindromes URAL - 1989 哈希+线段树
N - Subpalindromes URAL - 1989 这个是一个哈希+线段树,这个题目也不算特别难,但是呢,还比较有意思. 这个题目给你两个操作,一个是回答l~r 区间是不是回文,一个是对一个 ...
- NetCore项目实战篇04---集成IdentityService4
大家都知道我们的项目中已有web api,现在可以正式访问,不论任何人只要通过输入对应的api网址就可以访问到我们的api 资源,这样是很不安全的,我们需求对当前用户进行身份验证,因此我们在项目中使用 ...
- JDK基本库概述
看脚下,不断行,莫存顺逆. 剖析java的哪些源码 目前主要是java基本库的一些源码的分析,jvm工具的使用等等,后续可能还会结合hotspot源码来分析jvm原理,当然,这是一个比较高级的主题,根 ...
- 基于胜率矩阵的PageRank排序
在做博弈模型评估的时候,遇到一个问题是如何评价多个模型的优劣.例如我有训练好的三个围棋模型A,B,C,两两之间对打之后有一个胜负关系,如何对这三个模型进行排序呢?通常对于人类选手这种水平有波动的情 ...
- asp.net mvc entityframework sql server 迁移至 mysql方法以及遇到的问题
背景: 我原来的项目是asp.net mvc5 + entityframework 6.4 for sql server(localdb,sql server),现在需要把数据库切换成mysql,理论 ...
- jquery判断邮箱及密码是否输入正确的表单提交
jquery我接触的也不是很多,基本就是照着案例然后查相关方法做出来的,基本用了大概半天的时间,手打加查资料实现.具体如下,首先下载一个jquery包,网址是https://jquery.com/do ...
- redis 集群安装
redis集群安装 1.下载redis源码 2.解压并进入解压后的文件夹redis内 3.make,生成一系列的文件(mkreleasehdr.sh, redis-benchmark, redis-c ...
- Excel+Python:分组名单
各部门的社保.公积金.全勤奖.工衣.工龄奖.罚款等名单,要统计出来,A4纸横向排版.要么发群里通知,要么打印给相应主管.部门放一列,相应部门名单放一个cell里面. 公开透明后,人头不对.人名不对,各 ...
- [hdu5375 Gray code]DP
题意:给一个二进制码,其中有一些位上为'?',对每个问号确定是'0'还是'1',最后以它对应的格雷码来取数,第i位为1则取第i个数,求取得的数的和的最大值. 思路:二进制码B转换成格雷码G的方法是,G ...