FHQ Treap
基本概念
FHQ Treap 是由 fhq 神犇提出的一种数据结构,它可以实现 Treap 的功能,并且不需要 Treap 的旋转操作,所以 FHQ Treap 又被称为 无旋 Treap 或者 非旋 Treap 。
FHQ Treap 支持可持久化。
算法思想
无旋 Treap 的主要操作有分裂( \(split\) )和合并( \(merge\) )两种。顾名思义,无旋 Treap 保持树平衡的方式就是不断地将树按照某种方式分裂成两棵子树,再通过合并子树来调整节点的祖孙关系。而分裂又分为 按值分裂 和 按大小分裂 两种,通常情况下,我们会选择按值分裂。由于分裂的便捷性,无旋 Treap 的查询排名、前驱后继等操作都比 Treap 简洁很多。
无旋 Treap 和 Treap 一样,给每个节点都附上一个新的随机权值 \(key\) 。同样地,原本的点权满足二叉搜索树的性质,随机权值满足堆的性质。
更新
更新节点 \(x\) 的子树大小。与 Treap 不同的是,无旋 Treap 中点权相同的节点个数仅统计一次。
void push_up(int rt) { tree[rt].sz = tree[ls(rt)].sz + tree[rs(rt)].sz + 1; }
新建节点
新建一个权值为 \(val\) 的节点,返回其在数组中的下标。
int new_node(int v)
{
tree[++cnt].val = v;
tree[cnt].sz = 1;
tree[cnt].rd = rand();
return cnt;
}
分裂
分裂操作指将以 \(root\) 为根的子树分裂成两棵分别以 \(x, y\) 为根的子树,其中以 \(x\) 为根的子树满足所有节点的权值都小于等于 \(val\) ,以 \(y\) 为根的子树满足所有节点的权值都大于 \(val\) ,这就是按值分裂的规则。
具体的算法流程也很简单。假如 \(root = 0\) ,说明当前子树为空树,无法分裂,所以令 \(x = y = 0\) 。反之,若 \(root\) 的权值小于等于 \(val\) ,说明 \(root\) 应该划分在以 \(x\) 为根的子树内。因为无旋 Treap 是一棵二叉搜索树,所以 \(root\) 的左子树中任意一点的权值 \(\leq root\) 的权值 \(\leq val\) ,\(root\) 的左子树也应该划分入以 \(x\) 为根的子树。此时右子树里可能会出现点权比 \(val\) 大的节点,所以在右子树内继续递归分裂。\(root\) 的权值大于 \(val\) 的情况同理,将 \(root\) 及其右子树划分入以 \(y\) 为根的子树,继续在 \(root\) 的左子树内查找即可。
令以 \(x\) 为根的子树为树 \(1\) ,以 \(y\) 为根的子树为树 \(2\) 。我们定义函数 \(split(root, val, x, y)\) 表示当前要按照权值 \(val\) 分裂节点 \(root\) 及其子树,目前树 \(1\) 的子树根节点为 \(x\) ,树 \(2\) 的子树根节点为 \(y\) 。可以写出如下函数。
void split(int rt, int val, int &x, int &y)
{
if (!rt) { x = y = 0; return; }
if (tree[rt].val <= val) { x = rt; split(rs(rt), val, rs(rt), y); }
else { y = rt; split(ls(rt), val, x, ls(rt)); }
push_up(rt);
}
合并
合并操作指将以 \(x\) 为根的子树和以 \(y\) 为根的子树合并成一整棵树,并返回新树根节点的下标。合并得到的新树满足无旋 Treap 的性质,同时要求以 \(x\) 为根的子树中,所有节点的权值必须小于等于以 \(y\) 为根的子树中任意一点的权值。
假如 \(x\) 和 \(y\) 中存在至少一个 \(0\),那么相当于其中 \(1\) 棵或 \(0\) 棵子树构成了合并出来的新树,此时直接返回 \(x + y\) 即可。
反之,若 \(x\) 的随机权值大于 \(y\) 的随机权值,说明 \(x\) 必须是 \(y\) 的父节点。又因为 \(y\) 的点权大于 \(x\) 的点权,所以 \(y\) 必须是 \(x\) 的右儿子。将 \(x\) 的右儿子与以 \(y\) 为根的子树合并即可。
若 \(x\) 的随机权值小于等于 \(y\) 的随机权值,说明 \(x\) 必须是 \(y\) 的左儿子,将以 \(x\) 为根的子树与 \(y\) 的右儿子合并即可。因为合并操作需要前提,所以此处需要注意 参数传递的顺序。
根据以上逻辑,可以写出以下代码。
int merge(int x, int y)
{
if ((!x) || (!y)) return x | y;
if (tree[x].rd > tree[y].rd)
{
rs(x) = merge(rs(x), y);
push_up(x);
return x;
}
ls(y) = merge(x, ls(y));
push_up(y);
return y;
}
插入
在无旋 Treap 中插入一个点权为 \(val\) 的节点。
得益于无旋 Treap 的性质,插入等操作得到了巨大的简化。想要插入一个权值为 \(val\) 的节点,直接将整棵树按 \(val\) 分裂成两棵以 \(x, y\) 为根的子树。令新建节点的下标为 \(z\) ,此时按顺序合并 \(x, z, y\) 即可。
void ins(int v)
{
int x, y;
split(root, v, x, y);
root = merge(merge(x, new_node(v)), y);
}
删除
在无旋 Treap 中删除一个点权为 \(val\) 的节点,若存在多个点权为 \(val\) 的节点,只删除其中一个。
直接将整棵树按 \(val\) 分裂两棵以 \(x, z\) 为根的子树。再将以 \(x\) 为根的子树按 \(val - 1\) 分裂成两棵以 \(x, y\) 为根的子树。第一次操作时,以 \(x\) 为根的子树内所有点权均小于等于 \(val\) 。第二次操作后,以 \(x\) 为根的子树内所有点权均小于等于 \(val - 1\) 。也就是说,点权等于 \(val\) 的节点都被划分到了以 \(y\) 为根的子树内。此时在以 \(y\) 为根的子树内任意删除一个节点(通常选择根节点)即可,具体实现可以直接令以 \(y\) 为根的子树为其左子树和右子树合并得到的树,比原树恰好少了一个根节点。
void del(int v)
{
int x, y, z;
split(root, v, x, z);
split(x, v - 1, x, y);
y = merge(ls(y), rs(y));
root = merge(merge(x, y), z);
}
查询排名
查询无旋 Treap 中 \(val\) 数的排名,排名定义为无旋 Treap 中比 \(val\) 小的树的个数 \(+ 1\)。
将整棵树按 \(val - 1\) 分裂成两棵以 \(x, y\) 为根的子树。此时以 \(x\) 为根的子树中任意点权小于 \(val\) ,所以答案就是以 \(x\) 为根的子树的大小 \(+ 1\)。
int get_rk(int v)
{
int x, y;
split(root, v - 1, x, y);
int res = tree[x].sz + 1;
root = merge(x, y);
return res;
}
查询数值
查询无旋 Treap 中排名为 \(rk\) 的数。
从根节点开始查找,如果左子树的大小 \(+ 1 = rk\) ,说明当前节点就是要查找的数值,直接退出;如果左子树的大小 \(\geq rk\) ,说明要查找的数一定在左子树中,在左子树内继续查找;反之,要查找的树一定是右子树中排名为 $rk - $ 左子树大小 \(- 1\) 的数。此处不采用递归写法。
int get_val(int rk)
{
int rt = root;
while (rt)
{
if (tree[ls(rt)].sz + 1 == rk) return tree[rt].val;
else if (rk <= tree[ls(rt)].sz) rt = ls(rt);
else rk -= (tree[ls(rt)].sz + 1), rt = rs(rt);
}
return -1;
}
查找前驱
查询无旋 Treap 中数 \(val\) 的前驱,前驱定义为比 \(val\) 小的最大的数。
将整棵树按 \(val - 1\) 分裂成两棵以 \(x, y\) 为根的子树。此时以 \(x\) 为根的子树内所有点权一定都小于 \(val\) ,查找以 \(x\) 为根的子树内最大的点权即可。具体实现可以从 \(x\) 开始,不断地走到右儿子,直到走到叶子节点为止。
int pre(int v)
{
int x, y;
split(root, v - 1, x, y);
int rt = x;
while (rs(rt)) rt = rs(rt);
int res = tree[rt].val;
root = merge(x, y);
return res;
}
查找后继
查询无旋 Treap 中数 \(val\) 的后继,后继定义为比 \(val\) 大的最小的数。
将整棵树按 \(val\) 分裂成两棵以 \(x, y\) 为根的子树。此时以 \(y\) 为根的子树内所有点权一定都大于 \(val\) ,查找以 \(y\) 为根的子树内最小的点权即可。具体实现可以从 \(y\) 开始,不断地走到左儿子,直到走到叶子节点为止。
int nxt(int v)
{
int x, y;
split(root, v, x, y);
int rt = y;
while (ls(rt)) rt = ls(rt);
int res = tree[rt].val;
root = merge(x, y);
return res;
}
参考代码
#include <cstdio>
#include <cstdlib>
#include <ctime>
using namespace std;
#define ls(x) tree[x].l
#define rs(x) tree[x].r
const int maxn = 1e5 + 5;
struct node
{
int l, r, sz, rd, val;
} tree[maxn];
int n, root, cnt;
int new_node(int v)
{
tree[++cnt].val = v;
tree[cnt].sz = 1;
tree[cnt].rd = rand();
return cnt;
}
void push_up(int rt) { tree[rt].sz = tree[ls(rt)].sz + tree[rs(rt)].sz + 1; }
void split(int rt, int val, int &x, int &y)
{
if (!rt) { x = y = 0; return; }
if (tree[rt].val <= val) { x = rt; split(rs(rt), val, rs(rt), y); }
else { y = rt; split(ls(rt), val, x, ls(rt)); }
push_up(rt);
}
int merge(int x, int y)
{
if ((!x) || (!y)) return x | y;
if (tree[x].rd > tree[y].rd)
{
rs(x) = merge(rs(x), y);
push_up(x);
return x;
}
ls(y) = merge(x, ls(y));
push_up(y);
return y;
}
void ins(int v)
{
int x, y;
split(root, v, x, y);
root = merge(merge(x, new_node(v)), y);
}
void del(int v)
{
int x, y, z;
split(root, v, x, z);
split(x, v - 1, x, y);
y = merge(ls(y), rs(y));
root = merge(merge(x, y), z);
}
int get_rk(int v)
{
int x, y;
split(root, v - 1, x, y);
int res = tree[x].sz + 1;
root = merge(x, y);
return res;
}
int get_val(int rk)
{
int rt = root;
while (rt)
{
if (tree[ls(rt)].sz + 1 == rk) return tree[rt].val;
else if (rk <= tree[ls(rt)].sz) rt = ls(rt);
else rk -= (tree[ls(rt)].sz + 1), rt = rs(rt);
}
return -1;
}
int pre(int v)
{
int x, y;
split(root, v - 1, x, y);
int rt = x;
while (rs(rt)) rt = rs(rt);
int res = tree[rt].val;
root = merge(x, y);
return res;
}
int nxt(int v)
{
int x, y;
split(root, v, x, y);
int rt = y;
while (ls(rt)) rt = ls(rt);
int res = tree[rt].val;
root = merge(x, y);
return res;
}
int main()
{
srand(time(0));
scanf("%d", &n);
while (n--)
{
int opt, x;
scanf("%d%d", &opt, &x);
if (opt == 1) ins(x);
else if (opt == 2) del(x);
else if (opt == 3) printf("%d\n", get_rk(x));
else if (opt == 4) printf("%d\n", get_val(x));
else if (opt == 5) printf("%d\n", pre(x));
else printf("%d\n", nxt(x));
}
return 0;
}
例题选讲
文艺平衡树
令区间 \([l, r]\) 的中点为 \(m(l, r)\) ,建树的时候会让 \(m(l, r)\) 作为代表区间 \([l, r]\) 的结点,设 \(m(l, r) = k\) ,其左儿子为 \(m(l, k - 1)\) ,右儿子为 \(m(k + 1, r)\) 。
这样,以 \(k\) 为根的子树就代表着区间 \([l, r]\) ,并且它是一棵极度平衡的二叉树。每次操作区间 \([l, r]\) 就从 \(k\) 开始不断向下递归。
文艺平衡树需要 按大小分裂,大致思路与按值分裂相同,按 \(size\) 进行 \(split\) 操作定义为:将以 \(root\) 为根的子树分裂成两棵子树,其中一棵子树的大小 \(\leq size\) ,其余部分在另外一棵子树上。
操作思路也很简单,若左子树与根的大小总和 \(\leq size\) ,那么就将 \(root\) 和左子树并入以 \(x\) 为根的子树中,进入 \(root\) 的右子树分裂;反之,将 \(root\) 的右子树并入以 \(y\) 为根的子树中,并进入 \(root\) 的左子树继续分裂。
\(merge\) 操作则一模一样,区别在于用作文艺平衡树的时候需要先下传 \(lazy\) 标记再合并子树。
void split(int root, int size, int &x, int &y) {
if (!root) {
x = y = 0;
return;
} else {
push_down(root);
if (tree[tree[root].l].size < size) {
x = root;
split(tree[root].r, size - tree[tree[root].l].size - 1, tree[root].r, y);
} else {
y = root;
split(tree[root].l, size, x, tree[root].l);
}
update(root);
}
}
int merge(int x, int y) {
if (!x || !y) {
return x + y;
} else {
if (tree[x].key < tree[y].key) {
push_down(x);
tree[x].r = merge(tree[x].r, y);
update(x);
return x;
} else {
push_down(y);
tree[y].l = merge(x, tree[y].l);
update(y);
return y;
}
}
}
然后考虑翻转一个区间 \([l, r]\) 。
设将整棵树按 \(l - 1\) 分裂得到的两棵子树根分别为 \(x, y\) ,此时 \(x\) 树代表着区间 \([1, l - 1]\) , \(y\) 树代表着区间 \([l, n]\) 。再将 \(y\) 树按 \(r - l + 1\) 分裂成两棵子树 \(y, z\) 。此时 \(y\) 代表着区间 \([l, r]\) ,\(z\) 代表着区间 \([r + 1, n]\) 。
直接对 \(y\) 进行操作:将 \(y\) 的左右子树交换并取反 \(lazy\) 标记即可。别忘了最后还要将 \(x, y, z\) 合并回去。
最后对整棵树进行中序遍历,得到的序列就是最后的序列。
#include <cstdio>
#include <cstdlib>
#include <algorithm>
using namespace std;
const int maxn = 1e5 + 5;
struct node {
int l, r;
int val, key;
int size, lazy;
} tree[maxn];
int n, m, root, tot;
int newnode(int val) {
tot++;
tree[tot].val = val;
tree[tot].key = rand();
tree[tot].size = 1;
return tot;
}
void update(int k) {
tree[k].size = tree[tree[k].l].size + tree[tree[k].r].size + 1;
}
void push_down(int k) {
if (tree[k].lazy) {
swap(tree[k].l, tree[k].r);
tree[tree[k].l].lazy ^= 1;
tree[tree[k].r].lazy ^= 1;
tree[k].lazy = 0;
}
}
void split(int root, int size, int &x, int &y) {
if (!root) {
x = y = 0;
return;
} else {
push_down(root);
if (tree[tree[root].l].size < size) {
x = root;
split(tree[root].r, size - tree[tree[root].l].size - 1, tree[root].r, y);
} else {
y = root;
split(tree[root].l, size, x, tree[root].l);
}
update(root);
}
}
int merge(int x, int y) {
if (!x || !y) {
return x + y;
} else {
if (tree[x].key < tree[y].key) {
push_down(x);
tree[x].r = merge(tree[x].r, y);
update(x);
return x;
} else {
push_down(y);
tree[y].l = merge(x, tree[y].l);
update(y);
return y;
}
}
}
void reverse(int l, int r) {
int x, y, z;
split(root, l - 1, x, y);
split(y, r - l + 1, y, z);
tree[y].lazy ^= 1;
root = merge(merge(x, y), z);
}
void print(int root) {
if (!root) {
return;
} else {
push_down(root);
print(tree[root].l);
printf("%d ", tree[root].val);
print(tree[root].r);
}
}
int main() {
int l, r;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
root = merge(root, newnode(i));
}
for (int i = 1; i <= m; i++) {
scanf("%d%d", &l, &r);
reverse(l, r);
}
print(root);
puts("");
return 0;
}