平衡树与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节点的随机权值来确定合并方案。

  1. x, y两棵树都非空时:

  2. x,y都为空时:直接返回0。
  3. 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学习笔记的更多相关文章

  1. fhq treap 学习笔记

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

  2. FHQ treap学习(复习)笔记

    .....好吧....最后一篇学习笔记的flag它倒了..... 好吧,这篇笔记也鸽了好久好久了... 比赛前刷模板,才想着还是补个坑吧... FHQ,这个神仙(范浩强大佬),发明了这个神仙的数据结构 ...

  3. 左偏树 / 非旋转treap学习笔记

    背景 非旋转treap真的好久没有用过了... 左偏树由于之前学的时候没有写学习笔记, 学得也并不牢固. 所以打算写这么一篇学习笔记, 讲讲左偏树和非旋转treap. 左偏树 定义 左偏树(Lefti ...

  4. treap学习笔记

    treap是个很神奇的数据结构. 给你一个问题,你可以解决它吗? 这个问题需要treap这个数据结构. 众所周知,二叉查找树的查找效率低的原因是不平衡,而我们又不希望用各种奇奇怪怪的旋转来使它平衡,那 ...

  5. fhq treap抄袭笔记

    目录 碎碎念 点一下 注意!!! 模板 fhq treap 碎碎念 我咋感觉合并这么像左偏树呢 ps:难道你们的treap都是小头堆的吗 fhq真的是神人 现在看以前学的splay是有点恶心,尤其是压 ...

  6. Treap + 无旋转Treap 学习笔记

    普通的Treap模板 今天自己实现成功 /* * @Author: chenkexing * @Date: 2019-08-02 20:30:39 * @Last Modified by: chenk ...

  7. [Treap][学习笔记]

    平衡树 平衡树就是一种可以在log的时间复杂度内完成数据的插入,删除,查找第k大,查询排名,查询前驱后继以及其他许多操作的数据结构. Treap treap是一种比较好写,常数比较小,可以实现平衡树基 ...

  8. Treap-平衡树学习笔记

    平衡树-Treap学习笔记 最近刚学了Treap 发现这种数据结构真的是--妙啊妙啊~~ 咳咳.... 所以发一发博客,也是为了加深蒟蒻自己的理解 顺便帮助一下各位小伙伴们 切入正题 Treap的结构 ...

  9. 「FHQ Treap」学习笔记

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

  10. 「学习笔记」 FHQ Treap

    FHQ Treap FHQ Treap (%%%发明者范浩强年年NOI金牌)是一种神奇的数据结构,也叫非旋Treap,它不像Treap zig zag搞不清楚(所以叫非旋嘛),也不像Splay完全看不 ...

随机推荐

  1. 《Docker从入门到跑路》之存储卷介绍

    默认情况下,容器会随着用户删除而消失,包括容器里面的数据.如果我们要对容器里面的数据进行长久保存,就不得不引用存储卷的概念. 在容器中管理数据持久化主要有两种方式:1.数据卷(data volumes ...

  2. 探索Linux内核:Kconfig / kbuild的秘密

    探索Linux内核:Kconfig / kbuild的秘密 文章目录 探索Linux内核:Kconfig / kbuild的秘密 深入了解Linux配置/构建系统的工作原理 Kconfig kbuil ...

  3. Openwrt:基于MT7628/MT7688的PWM驱动

    前言 MT7628/MT7688的PWM驱动相关资料较少,官方的datasheet基本也是一堆寄存器,啃了许久,终于嚼出了味道.由于PWM存在IO口复用的问题,所以要提前配置好GPIO的工作方式,不然 ...

  4. 一文搞懂HMM(隐马尔可夫模型)-转载

    写在文前:原博文地址:https://www.cnblogs.com/skyme/p/4651331.html 什么是熵(Entropy) 简单来说,熵是表示物质系统状态的一种度量,用它老表征系统的无 ...

  5. java8 新特性Stream流的应用

    作为一个合格的程序员,如何让代码更简洁明了,提升编码速度尼. 今天跟着我一起来学习下java 8  stream 流的应用吧. 废话不多说,直入正题. 考虑以下业务场景,有四个人员信息,我们需要根据性 ...

  6. case when的使用-解决分表查数据给某一个字段

    一个表中存的是目前有效的菜单,另外一个表中存的是有效菜单的历史更改数据 需要查询历史数据的时候,带上访问的历史数据菜单名称 SELECT msg.msg_id, msg.from_user_name, ...

  7. java 生成随机字符串

    1.生成之指定位数的随机字符串 /** * 随机基数 */ private static char[] charset = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h ...

  8. 线上Kafka突发rebalance异常,如何快速解决?

    文章首发于[陈树义的博客],点击跳转到原文<线上Kafka突发rebalance异常,如何快速解决?> Kafka 是我们最常用的消息队列,它那几万.甚至几十万的处理速度让我们为之欣喜若狂 ...

  9. spark机器学习从0到1支持向量机SVM(五)

        分类 分类旨在将项目分为不同类别. 最常见的分类类型是二元分类,其中有两类,通常分别为正数和负数. 如果有两个以上的类别,则称为多类分类. spark.mllib支持两种线性分类方法:线性支持 ...

  10. AJAX三

    三.ajax 4.代参数的get方法 ①服务器 ②ajax代码 xhr.open("get",url,true) url="/demo/get_login?uname=& ...