介绍
平衡树
平衡树,又称treap,是树(tree)以及堆(heap)的合称,具体表现为形式上它是一棵二叉树,而性质上它又满足堆的性质。与普通的 BST(Binary Search Tree) 相比,它由于具有了堆的性质从而使二叉树平衡,避免了当数据单调时BST退化成链的劣势,将时间复杂度降到了均摊 \(O(\log n)\) 的级别
FHQ-treap
FHQ-treap是一种无旋平衡树,拥有码量小、容易写的优点。具体通过 \(split\) 和 \(merge\) 两种操作实现,即将整棵树分裂为两棵树的操作与将两棵树合并为一棵完整的平衡树的,对节点进行操作时视情况将整棵平衡树按权值或按排名分裂,从而避免了普通treap里面繁琐的旋转操作。
核心操作
upd
功能
很简单,更新以 \(p\) 为根节点的树的大小信息,这个信息在与排名有关的操作中很有用
code
inline void upd(int p) {siz[p] = siz[lc[p]] + siz[rc[p]] + 1;}
split
功能
将根节点为 \(p\) 的树分裂为根节点为 \(l\) 和 \(r\) 的两棵 treap,并且使树 \(l\) 上的所有权值都小于等于 \(v\),树 \(r\) 上的所有权值都大于 \(v\), 这两棵树仍然满足 \(treap\) 的性质。
code
void split(int p, int v, int &l, int &r) {
if(!p) {l = r = 0; return;}
if(val[p] <= v) l = p, split(rc[p], v, rc[p], r);
else r = p, split(lc[p], v, l, lc[p]);
upd(p);
}
理解
如图,如果从根也就是24按38分裂的话,调用的代码如下
split(rt, 38, x, y);
因为 \(38 > 24\) ,所以会将 \(24\) 暂时赋值给 \(x\),但是由于BST的性质,\(39\) 的左儿子小于 \(39\),也可能小于 \(v\) ,在这个例子中也就是 \(33\),所以将 \(rc_p\) 作为参数 \(l\) 传进下一层递归中,因为 \(l\) 和 \(r\) 是引用的,所以可以将 \(39\) 的左儿子赋值给 \(rc_p\) ,这样的话就能使分裂后的两棵树满足BST的性质。
这里给的是按权值分裂的代码,如果要实现按排名分裂,就需要判断 \(siz_{lc_p}\) 与排名 \(k\) 的关系了
merge
功能
可以理解为 split
的逆运算,将根节点为 \(l\) 和 \(r\) 的两棵树合并并且根据 \(l\) 和 \(r\) 的随机权值将这棵树的根节点赋为 \(l\) 或 \(r\)
code
inline int merge(int l, int r) {
if(!l || !r) return l + r;
if(rd[l] <= rd[r]) {
lc[r] = merge(l, lc[r]);
upd(r); return r;
}
else {
rc[l] = merge(rc[l], r);
upd(l); return l;
}
}
理解
我们在记忆或者说理解 split
和 merge
的函数操作时只需要谨记一点:BST的性质,即左子树上的权值永远小于根节点,右子树上的权值永远大于根节点
对 merge
这个函数的操作我就不单独说了
ins
code
inline void rand(int &xx) {xx = (rnd = (rnd * 73819) % 23333);}
inline void cre(int v) {
rand(rd[++ cnt]);
val[cnt] = v;
siz[cnt] = 1;
}
inline void ins(int v) {
split(rt, v, x, y);
cre(v);
rt = merge(merge(x, cnt), y);
}
理解
cre
以及 rand
没啥好说的,新建一个节点,节点总数 \(+1\) ,给这个节点赋一个随机值,更新权值以及这个节点的大小。
需要注意 cre
的功能仅仅为新建一个节点的信息,执行完以后这个节点独立于整棵树之外,在 insert
函数中才会将它插入到整棵树当中
例题
P3369 【模板】普通平衡树
Description
您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
1.插入 \(x\) 数
2.删除 \(x\) 数(若有多个相同的数,因只删除一个)
3.查询 \(x\) 数的排名(排名定义为比当前数小的数的个数 \(+1\) )
4.查询排名为 \(x\) 的数
5.求 \(x\) 的前驱(前驱定义为小于 \(x\),且最大的数)
6.求 \(x\) 的后继(后继定义为大于 \(x\),且最小的数)
Solution
平衡树板题,关于排名的一切操作都用到 \(siz\) 就好了
Code
#include<iostream>
#include<stdio.h>
#define maxn 100005
using namespace std;
int lc[maxn], rc[maxn], siz[maxn], val[maxn], rd[maxn];
int rnd = 1978347, x, y, z, rt, cnt;
inline void rand(int &xx) {xx = (rnd = (rnd * 73819) % 23333);}
inline void cre(int v) {
rand(rd[++ cnt]);
val[cnt] = v;
siz[cnt] = 1;
}
inline void upd(int p) {siz[p] = siz[lc[p]] + siz[rc[p]] + 1;}
inline void split(int p, int v, int &l, int &r) {
if(!p) {l = r = 0; return;}
if(val[p] <= v) l = p, split(rc[p], v, rc[p], r);
else r = p, split(lc[p], v, l, lc[p]);
upd(p);
}
inline int merge(int l, int r) {
if(!l || !r) return l + r;
if(rd[l] <= rd[r]) {
lc[r] = merge(l, lc[r]);
upd(r); return r;
}
else {
rc[l] = merge(rc[l], r);
upd(l); return l;
}
}
inline void ins(int v) {
split(rt, v, x, y);
cre(v);
rt = merge(merge(x, cnt), y);
}
inline void del(int v) {
split(rt, v, x, y);
split(x, v - 1, x, z);
z = merge(lc[z], rc[z]);
rt = merge(merge(x, z), y);
}
inline int findrank(int v) {
split(rt, v - 1, x, y);
int tmp = siz[x] + 1;
rt = merge(x, y);
return tmp;
}
inline int kth(int k) {
int p = rt;
while(p) {
if(siz[lc[p]] + 1 == k) break;
else if(siz[lc[p]] + 1 >= k) p = lc[p];
else k -= siz[lc[p]] + 1, p = rc[p];
}
return val[p];
}
inline int front(int v){
split(rt, v - 1, x, y);
int p = x;
while(rc[p]) p = rc[p];
rt = merge(x, y);
return val[p];
}
inline int back(int v){
split(rt, v, x, y);
int p = y;
while(lc[p]) p = lc[p];
rt = merge(x, y);
return val[p];
}
int main(){
int n, opt, xx;
scanf("%d", &n);
for(int i = 1; i <= n; ++ i) {
scanf("%d%d", &opt, &xx);
if(opt == 1) ins(xx);
else if(opt == 2) del(xx);
else if(opt == 3) printf("%d\n", findrank(xx));
else if(opt == 4) printf("%d\n", kth(xx));
else if(opt == 5) printf("%d\n", front(xx));
else printf("%d\n", back(xx));
}
return 0;
}
P1503 鬼子进村
Description
有 \(n\) 个房子,排成一排,第 \(i\) 个房子只与第 \(i - 1\) 和 \(i + 1\) 个相连,下面有 \(m\) 个信息:
1.若消息为 D x
:鬼子将 \(x\) 号房子摧毁了,地道被堵上。
2.若消息为 R
:村民们将鬼子上一个摧毁的房子修复了。
3.若消息为 Q x
:有一名士兵被围堵在 \(x\) 号房子中,输出他能到达的房子数量。
Solution
1.若消息为 D x
,将 \(x\) 插入平衡树。
2.若消息为 R
,将上一个 \(x\) 删除。
3.若消息为 Q x
,根据 \(x\) 的前驱和后继求能到达的房子树
注意,在开始将 \(0\) 和 \(n + 1\) 就插入平衡树
Code
#include<iostream>
#include<cstdio>
#include<stack>
#define maxn 50004
using namespace std;
int lc[maxn], rc[maxn], val[maxn], siz[maxn], rd[maxn], rnd = 97231, cnt, rt, x, y, z;
bool des[maxn];
inline void rand(int &x) {x = (rnd = (rnd * 233531) % 1000000);}
inline void cre(int v) {
rand(rd[++ cnt]);
val[cnt] = v;
siz[cnt] = 1;
}
inline void upd(int p) {siz[p] = siz[lc[p]] + siz[rc[p]] + 1;}
inline void split(int p, int v, int &l, int &r) {
if(!p) {l = r = 0; return;}
if(v >= val[p]) l = p, split(rc[p], v, rc[p], r);
else r = p, split(lc[p], v, l, lc[p]);
upd(p);
}
inline int merge(int l, int r) {
if(!l || !r) return l + r;
if(rd[l] <= rd[r]) {
lc[r] = merge(l, lc[r]);
upd(r); return r;
}
else {
rc[l] = merge(rc[l], r);
upd(l); return l;
}
}
inline void ins(int v) {
split(rt, v, x, y);
cre(v);
rt = merge(merge(x, cnt), y);
}
inline void del(int v) {
split(rt, v - 1, x , y);
split(y, v, y, z);
y = merge(lc[y], rc[y]);
rt = merge(merge(x, y), z);
}
inline int front(int v) {
split(rt, v - 1, x, y);
int p = x;
while(rc[p]) p = rc[p];
rt = merge(x, y);
return val[p];
}
inline int back(int v) {
split(rt, v, x, y);
int p = y;
// cout << lc[p] << "n\n";
while(lc[p]) p = lc[p];
rt = merge(x, y);
return val[p];
}
stack<int> st;
int n, m, q;
char tmp;
int main() {
ios :: sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n >> m;
ins(0);
ins(n + 1);
for(int i = 1; i <= m; ++ i) {
cin >> tmp;
if(tmp == 'D') cin >> q, ins(q), st.push(q), des[q] = 1;
else if(tmp == 'Q'){
cin >> q;
if(des[q]) printf("0\n");
else printf("%d\n", back(q) - front(q) - 1);
}
else del(st.top()), des[st.top()] = 0, st.pop();
}
return 0;
}
标签:int,siz,void,笔记,treap,maxn,split,inline,FHQ
From: https://www.cnblogs.com/8atemak1r/p/16736609.html