FHQ-Treap详细解析
FHQ-Treap(无旋 Treap)
1 介绍
FHQ-Treap 是一种不带旋的平衡树,和带旋版本一样都是用随机优先级完成平衡。不同的是,FHQ-Treap 是通过分裂和合并来插入和删除,也可以用于文艺平衡树。
2 算法内容
2.1 分裂
我们有一颗树,我们要分成两部分,一部分根为 \(x\),权值都小于等于 \(v\);一部分根为 \(y\),权值都大于 \(v\)。
我们考虑从根出发,如果根小于等于 \(v\),说明根和左子树均为 \(x\) 的子树,将 \(x\) 设为根,并递归右子树, 将分裂出的左半边设为根的右子树,剩余的为 \(y\)。
否则说明根和右子树都是 \(y\) 的子树,将 \(y\) 设为根,并递归左子树, 将分裂出的右半边设为根的左子树,剩余的为 \(x\)。

由于分裂时父子关系没有改变,所以不用担心堆权值的问题。
void split(int u, int val, int &x, int &y) {
if(!u) {x = y = 0; return;}
if(key(u) <= val) {
x = u; split(rc(u), val, rc(u), y);
} else {
y = u; split(lc(u), val, x, lc(u));
}
push_up(u);
}
2.2 合并
我们有两棵树,左边的权值都小于等于右边的,我们要把它合并,并返回根。
我们考虑如果左边的随机权值比右边的更小,那左边的肯定要当根,然后合并右子树和右边的树的即可。
反之,右边的当根,合并左子树和左边的树的即可。
如果一边是空的,另一边就是根了。

int merge(int x, int y) {
if(!x || !y) return x + y;
int p;
if(rk(x) < rk(y)) p = x, rc(p) = merge(rc(p), y);
else p = y, lc(p) = merge(x, lc(p));
push_up(p);
return p;
}
2.3 插入
有了分裂和合并就好插入了。我们可以想到把权值小于等于 \(v\) 和大于 \(v\) 的分开,再新建一个节点,最后把这三块合并即可。
void insert(const int &ky) {
node[++cnt] = {0, 0, randint(), 1, ky};
int x, y;
split(rt, ky, x, y);
rt = merge(merge(x, cnt), y);
}
2.4 删除
其实就是先分成等于 \(v\) 的(根 \(y\)),大于 \(v\) 的(根 \(z\)),小于 \(v\) 的(根 \(x\)),然后把 \(x\)、\(y\) 的左子树、\(y\) 的右子树、\(z\) 合并起来就行了,这样就只删了一个 \(v\)。
void erase(const int &ky) {
int x, y, z;
split(rt, ky, x, y);
split(x, ky - 1, x, z);
rt = merge(merge(x, merge(lc(z), rc(z))), y);
}
2.5 第 k 小,排名,前驱,后继
跟普通的 BST 都一样,就不讲了。
int query_rk(int u, const int &ky) {
if(!u) return 1;
if(ky <= key(u)) return query_rk(lc(u), ky);
else return sz(lc(u)) + 1 + query_rk(rc(u), ky);
}
int query_kth(int u, int k) {
if(sz(lc(u)) >= k) return query_kth(lc(u), k);
if(sz(lc(u)) + 1 == k) return key(u);
return query_kth(rc(u), k - sz(lc(u)) - 1);
}
int pre(int u, const int &v) {
if(!u) return -1e9;
if(key(u) < v) return max(key(u), pre(rc(u), v));
return pre(lc(u), v);
}
int suc(int u, const int &v) {
if(!u) return 1e9;
if(key(u) > v) return min(key(u), suc(lc(u), v));
return suc(rc(u), v);
}
3 总体代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e5 + 5;
class Treap {
private:
mt19937 engine;
struct Node {
int lc, rc, rk, sz;
int key;
} node[N];
int cnt;
int rt;
int &lc(int u) {return node[u].lc;}
int &rc(int u) {return node[u].rc;}
int &rk(int u) {return node[u].rk;}
int &sz(int u) {return node[u].sz;}
int &key(int u) {return node[u].key;}
void push_up(int u) {
sz(u) = sz(lc(u)) + sz(rc(u)) + 1;
}
int randint(int minn = 0x80000000, int maxn = 0x7FFFFFFF) {
std::uniform_int_distribution<int> dist(minn, maxn);
return dist(engine);
}
void split(int u, int val, int &x, int &y) {
if(!u) {x = y = 0; return;}
if(key(u) <= val) {
x = u; split(rc(u), val, rc(u), y);
} else {
y = u; split(lc(u), val, x, lc(u));
}
push_up(u);
}
int merge(int x, int y) {
if(!x || !y) return x + y;
int p;
if(rk(x) < rk(y)) p = x, rc(p) = merge(rc(p), y);
else p = y, lc(p) = merge(x, lc(p));
push_up(p);
return p;
}
int query_rk(int u, const int &ky) {
if(!u) return 1;
if(ky <= key(u)) return query_rk(lc(u), ky);
else return sz(lc(u)) + 1 + query_rk(rc(u), ky);
}
int query_kth(int u, int k) {
if(sz(lc(u)) >= k) return query_kth(lc(u), k);
if(sz(lc(u)) + 1 == k) return key(u);
return query_kth(rc(u), k - sz(lc(u)) - 1);
}
int pre(int u, const int &v) {
if(!u) return -1e9;
if(key(u) < v) return max(key(u), pre(rc(u), v));
return pre(lc(u), v);
}
int suc(int u, const int &v) {
if(!u) return 1e9;
if(key(u) > v) return min(key(u), suc(lc(u), v));
return suc(rc(u), v);
}
public:
Treap(int seed = random_device{}()) {
rt = 0, cnt = 0;
engine.seed(seed);
}
void insert(const int &ky) {
node[++cnt] = {0, 0, randint(), 1, ky};
int x, y;
split(rt, ky, x, y);
rt = merge(merge(x, cnt), y);
}
void erase(const int &ky) {
int x, y, z;
split(rt, ky, x, y);
split(x, ky - 1, x, z);
rt = merge(merge(x, merge(lc(z), rc(z))), y);
}
int query_rk(const int &ky) {
return query_rk(rt, ky);
}
int query_kth(int k) {
return query_kth(rt, k);
}
int pre(const int &ky) {
return pre(rt, ky);
}
int suc(const int &ky) {
return suc(rt, ky);
}
};
Treap t;
int main() {
ios::sync_with_stdio(0), cin.tie(0);
int n; cin >> n;
while(n--) {
int op, x; cin >> op >> x;
if(op == 1) t.insert(x);
else if(op == 2) t.erase(x);
else if(op == 3) cout << t.query_rk(x) << "\n";
else if(op == 4) cout << t.query_kth(x) << "\n";
else if(op == 5) cout << t.pre(x) << "\n";
else cout << t.suc(x) << "\n";
}
return 0;
}
4 补充:文艺平衡树
以P3391 【模板】文艺平衡树为例。
4.1 思路
我们知道线段树可以维护区间问题,就是用二叉树维护每一段的区间。我们注意到平衡树也是一种二叉树,如果把维护的关键字换成下标,那就可以将一段区间分成左、根、右三段,也可以实现线段树的功能。
我们知道区间反转可以将左挂右,右挂左,并递归到子树继续干。但这样明显是 \(O(n)\) 的,不可接受。
4.1.1 懒标记
我们像线段树一样引入懒标记,以及 \(\operatorname{pushdown}\) 函数。我们在看到一个区间时就直接打上标记,需要时再 \(\operatorname{pushdown}\),就可以加快速度。
4.1.2 提取区间
由于 FHQ-Treap 有分裂功能,但是那是按权值分裂的,所以我们可以写一个按大小分裂的版本。设要分裂出 \(k\) 个节点至 \(x\),剩下的到 \(y\)。步骤:
- 如果左子树的大小大于 \(k\),说明根节点要被划分到 \(y\),继续递归左子树,分别保存到 \(x\) 和根节点的左孩子。
- 如果小于 \(k\),说明根节点要到 \(x\),递归右子树,注意 \(k\) 要减去左子树的大小再减去根节点的一,分别保存到根的右孩子和 \(y\)。
这样我们就可以分裂出 \([l,r]\) 的区间了,接着打上标记即可。
注意:在分裂之前要先 \(\operatorname{pushdown}\)。
4.1.3 合并
这和普通的没什么区别,就是进入函数一开始要 \(\operatorname{pushdown}\)。
4.1.4 中序遍历
就是普通的,但在进入子树前要 \(\operatorname{pushdown}\)。
4.1.5 建树
首先我们可以一个一个插入,复杂度 \(O(n\log n)\)。
但是我发现一个很重要的性质:中序遍历的下标就是 \([1, n]\),随机权值还满足堆性质。这不就是笛卡尔树吗?
众所周知,笛卡尔树有利用单调栈 \(O(n)\) 建树的算法,所以文艺平衡树也可以使用这种方式。
4.2 拓展
我们注意到懒标记不仅可以存反转的标记,也可以存加法、乘法等线段树的功能,所以也是一个非常强大的数据结构,可以结合许多算法来优化,也可以实现树套树等复杂数据结构。

FHQ-Treap 是一种不带旋的平衡树,和带旋版本一样都是用随机优先级完成平衡。不同的是,FHQ-Treap 是通过分裂和合并来插入和删除,也可以用于文艺平衡树。
浙公网安备 33010602011771号