基本概念

\(FHQ \ Treap\) 是由 \(fhq\) 神犇提出的一种数据结构,它可以实现 \(Treap\) 的功能,并且不需要 \(Treap\) 的旋转操作,所以 \(FHQ \ Treap\) 又被称为 无旋 \(Treap\) 或者 非旋 \(Treap\) ,下文统称无旋 \(Treap\) 。

相较于 \(Splay\) 而言,无旋 \(Treap\) 的优势在于 可持久化 ,但是据大佬称无旋 \(Treap\) 在作为 \(LCT\) 的辅助树时会比 \(Splay\) 时间复杂度多了一个 \(log\) ,所以 \(LCT\) 的辅助树一般会选用 \(Splay\) 。因为无旋 \(Treap\) 优秀的 代码简洁性 以及其不算慢的性能,无旋 \(Treap\) 和 \(Splay\) 被并称为 \(OI\) 两大主流平衡树

算法思想

无旋 \(Treap\) 的主要操作有分裂( \(split\) )和合并( \(merge\) )两种。顾名思义,无旋 \(Treap\) 保持树平衡的方式就是不断地将树按照某种方式分裂成两棵子树,再通过合并子树来调整节点的祖孙关系。而分裂又分为 按值分裂按大小分裂 两种,通常情况下,我们会选择按值分裂。由于分裂的便捷性,无旋 \(Treap\) 的查询排名、前驱后继等操作都比 \(Treap\) 简洁很多。

无旋 \(Treap\) 和 \(Treap\) 一样,给每个节点都附上一个新的随机权值 \(key\) 。同样地,原本的点权满足二叉搜索树的性质,随机权值满足堆的性质。

更新

更新节点 \(x\) 的子树大小。与 \(Treap\) 不同的是,无旋 \(Treap\) 中点权相同的节点个数仅统计一次。

void update(int k) {
tree[k].size = tree[tree[k].l].size + tree[tree[k].r].size + 1;
}


新建节点

新建一个权值为 \(val\) 的节点,返回其在数组中的下标。

int newnode(int val) {
tree[++cnt].val = val;
tree[cnt].size = 1;
tree[cnt].key = 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 root, int val, int &x, int &y) {
if (!root) {
x = y = 0;
return;
}
if (tree[root].val <= val) {
x = root;
split(tree[root].r, val, tree[root].r, y);
} else {
y = root;
split(tree[root].l, val, x, tree[root].l);
}
update(root);
}


合并

合并操作指将以 \(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].key > tree[y].key) {
tree[x].r = merge(tree[x].r, y);
update(x);
return x;
} else {
tree[y].l = merge(x, tree[y].l);
update(y);
return y;
}
}


插入

在无旋 \(Treap\) 中插入一个点权为 \(val\) 的节点。

得益于无旋 \(Treap\) 的性质,插入等操作得到了巨大的简化。想要插入一个权值为 \(val\) 的节点,直接将整棵树按 \(val\) 分裂成两棵以 \(x, y\) 为根的子树。令新建节点的下标为 \(z\) ,此时按顺序合并 \(x, z, y\) 即可。

void insert(int val) {
z = newnode(val);
split(root, val, x, y);
root = merge(merge(x, z), y);
}


删除

在无旋 \(Treap\) 中删除一个点权为 \(val\) 的节点,若存在多个点权为 \(val\) 的节点,只删除其中一个。

直接将整棵树按 \(val\) 分裂两棵以 \(x, z\) 为根的子树。再将以 \(x\) 为根的子树按 \(val - 1\) 分裂成两棵以 \(x, y\) 为根的子树。第一次操作时,以 \(x\) 为根的子树内所有点权均小于等于 \(val\) 。第二次操作后,以 \(x\) 为根的子树内所有点权均小于等于 \(val - 1\) 。也就是说,点权等于 \(val\) 的节点都被划分到了以 \(y\) 为根的子树内。此时在以 \(y\) 为根的子树内任意删除一个节点(通常选择根节点)即可,具体实现可以直接令以 \(y\) 为根的子树为其左子树和右子树合并得到的树,比原树恰好少了一个根节点。

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


查询排名

查询无旋 \(Treap\) 中 \(val\) 数的排名,排名定义为无旋 \(Treap\) 中比 \(val\) 小的树的个数 \(+ 1\)。

将整棵树按 \(val - 1\) 分裂成两棵以 \(x, y\) 为根的子树。此时以 \(x\) 为根的子树中任意点权小于 \(val\) ,所以答案就是以 \(x\) 为根的子树的大小 \(+ 1\)。

void rank(int val) {
split(root, val - 1, x, y);
printf("%d\n", tree[x].size + 1);
root = merge(x, y);
}


查询数值

查询无旋 \(Treap\) 中排名为 \(rk\) 的数。

从根节点开始查找,如果左子树的大小 \(+ 1 = rk\) ,说明当前节点就是要查找的数值,直接退出;如果左子树的大小 \(\geq rk\) ,说明要查找的数一定在左子树中,在左子树内继续查找;反之,要查找的树一定是右子树中排名为 $rk - $ 左子树大小 \(- 1\) 的数。此处不采用递归写法。

void find(int rk) {
int rt = root;
while (rt) {
if (tree[tree[rt].l].size + 1 == rk) {
break;
}
if (tree[tree[rt].l].size >= rk) {
rt = tree[rt].l;
} else {
rk -= (tree[tree[rt].l].size + 1);
rt = tree[rt].r;
}
}
printf("%d\n", tree[rt].val);
}


查找前驱

查询无旋 \(Treap\) 中数 \(val\) 的前驱,前驱定义为比 \(val\) 小的最大的数。

将整棵树按 \(val - 1\) 分裂成两棵以 \(x, y\) 为根的子树。此时以 \(x\) 为根的子树内所有点权一定都小于 \(val\) ,查找以 \(x\) 为根的子树内最大的点权即可。具体实现可以从 \(x\) 开始,不断地走到右儿子,直到走到叶子节点为止。

void pre(int val) {
split(root, val - 1, x, y);
int rt = x;
while (tree[rt].r) {
rt = tree[rt].r;
}
printf("%d\n", tree[rt].val);
root = merge(x, y);
}


查找后继

查询无旋 \(Treap\) 中数 \(val\) 的后继,后继定义为比 \(val\) 大的最小的数。

将整棵树按 \(val\) 分裂成两棵以 \(x, y\) 为根的子树。此时以 \(y\) 为根的子树内所有点权一定都大于 \(val\) ,查找以 \(y\) 为根的子树内最小的点权即可。具体实现可以从 \(y\) 开始,不断地走到左儿子,直到走到叶子节点为止。

void nxt(int val) {
split(root, val, x, y);
int rt = y;
while (tree[rt].l) {
rt = tree[rt].l;
}
printf("%d\n", tree[rt].val);
root = merge(x, y);
}


注意事项

请注意,在每一次分裂以后,都要重新将整棵树合并起来,同时更新根节点的下标,否则无旋 \(Treap\) 将会被拆分成多棵不相干扰的子树,这不是我们希望看到的结果。

参考代码

​例题链接​

#include <cstdio>
#include <cstdlib>
using namespace std;
#define rank Rank

const int maxn = 1e5 + 5;

struct node {
int l, r, size, key, val;
} tree[maxn];

int n, root, cnt;
int x, y, z;

int newnode(int val) {
tree[++cnt].val = val;
tree[cnt].size = 1;
tree[cnt].key = rand();
return cnt;
}

void update(int k) {
tree[k].size = tree[tree[k].l].size + tree[tree[k].r].size + 1;
}

void split(int root, int val, int &x, int &y) {
if (!root) {
x = y = 0;
return;
}
if (tree[root].val <= val) {
x = root;
split(tree[root].r, val, tree[root].r, y);
} else {
y = root;
split(tree[root].l, val, x, tree[root].l);
}
update(root);
}

int merge(int x, int y) {
if (!x || !y) {
return x + y;
}
if (tree[x].key > tree[y].key) {
tree[x].r = merge(tree[x].r, y);
update(x);
return x;
} else {
tree[y].l = merge(x, tree[y].l);
update(y);
return y;
}
}

void insert(int val) {
z = newnode(val);
split(root, val, x, y);
root = merge(merge(x, z), y);
}

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

void rank(int val) {
split(root, val - 1, x, y);
printf("%d\n", tree[x].size + 1);
root = merge(x, y);
}

void find(int rk) {
int rt = root;
while (rt) {
if (tree[tree[rt].l].size + 1 == rk) {
break;
}
if (tree[tree[rt].l].size >= rk) {
rt = tree[rt].l;
} else {
rk -= (tree[tree[rt].l].size + 1);
rt = tree[rt].r;
}
}
printf("%d\n", tree[rt].val);
}

void pre(int val) {
split(root, val - 1, x, y);
int rt = x;
while (tree[rt].r) {
rt = tree[rt].r;
}
printf("%d\n", tree[rt].val);
root = merge(x, y);
}

void nxt(int val) {
split(root, val, x, y);
int rt = y;
while (tree[rt].l) {
rt = tree[rt].l;
}
printf("%d\n", tree[rt].val);
root = merge(x, y);
}

int main() {
int opt, x;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d%d", &opt, &x);
if (opt == 1) {
insert(x);
} else if (opt == 2) {
del(x);
} else if (opt == 3) {
rank(x);
} else if (opt == 4) {
find(x);
} else if (opt == 5) {
pre(x);
} else if (opt == 6) {
nxt(x);
}
}
return 0;
}


例题选讲

文艺平衡树

​例题链接​

请写出一个可以翻转区间的数据结构,如果还没有学习过文艺平衡树,可以前往 ​​这篇博文​​ 最后的例题选讲部分进行初步了解。

文艺平衡树也可以用无旋 \(Treap\) 来实现,思想与用 \(Splay\) 实现的文艺平衡树大致相同。我们令区间 \([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\) 开始不断向下递归。

接着,我们考虑无旋 \(Treap\) 的操作。这里采用 按大小分裂 的无旋 \(Treap\) 。大致思路与按值分裂相同,按 \(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;
}