【算法】平衡树
1. 二叉搜索树
1.1 简介
二叉搜索树是一种二叉树的树形数据结构,能将数据存储在一个树形结构上。
其满足的性质:
- 二叉搜索树为一棵二叉树,每个节点至多有 \(2\) 个子节点;
- 二叉搜索树中任意一个节点的左儿子值小于本节点值,右儿子值大于本节点值(左右取等亦可);
- 二叉搜索树的左右子树均为二叉搜索树。
以上的引申结论有:
- 若二叉搜索树的左子树不为空,则其左子树上所有点的值均小于其根节点的值;
- 若二叉搜索树的右子树不为空,则其右子树上所有点的值均大于其根节点的值。
基本的二叉搜索树可以完成插入,删除,按值查排名,按排名查值,前驱、后继等等。
1.2 理论
想象如何在二叉搜索树上插入一个数。
考虑在搜索树上二分,每次比较当前插入值与经过节点的值来判断插入的位置。删除亦是如此。
而按值查排名,按排名查值等可以通过维护每个节点子树大小 \(siz\) 来求解。
-
按值查排名:类似于插入操作来找到值的位置,若走右儿子,将排名加上左儿子的子树大小加一。
-
按排名查值:若左儿子的子树大小大于等于当前排名,则值在左子树;#若右儿子的子树大小小于当前排名,则值在右子树,排名更新为当前排名-左子树-1;
前驱为小于当前值的最大值,后继为大于当前值的最小值。
所以前驱为当前值的排名+1的值,后继为当前值+1的排名的值。
1.3 实现
1.3.1 插入
void add(int x, int v) {
tree[x].siz++;
if(tree[x].val == v) {
tree[x].cnt++;
return ;
}
if(v < tree[x].val) {//左子树
if(tree[x].ls) {//有左子树
add(tree[x].ls, v);
} else {//无左子树
tree[++num].val = v;
tree[num].cnt = tree[num].siz = 1;
tree[x].ls = num;
}
}
else {//右子树
if(tree[x].rs) {
add(tree[x].rs, v);
} else {
tree[++num].val = v;
tree[num].cnt = tree[num].siz = 1;
tree[x].rs = num;
}
}
}
删除类似于插入,不过多赘述。
1.3.2 按值查排名
int lrank(int x, int v) {
if(x == 0) return 0;
if(v == tree[x].val) {
return tree[tree[x].ls].siz;
}
if(v < tree[x].val) {
return lrank(tree[x].ls, v);
}
return lrank(tree[x].rs, v) + tree[tree[x].ls].siz + tree[x].cnt;
}
1.3.3 按排名查值
int kth(int x, int rk) {
if(x == 0) return INF;
if(rk <= tree[tree[x].ls].siz) {
return kth(tree[x].ls, rk);
}
if(rk <= tree[tree[x].ls].siz + tree[x].cnt) {
return tree[x].val;
}
return kth(tree[x].rs, rk - tree[tree[x].ls].siz - tree[x].cnt);
}
1.3.4 前驱
int pre(int x, int v, int ans) {
if(v <= tree[x].val) {
if(tree[x].ls) {
return pre(tree[x].ls, v, ans);
} else return ans;
} else {
if(!tree[x].rs) {
return (tree[x].val < v) ? tree[x].val : ans;
}
if(tree[x].cnt) {
return pre(tree[x].rs, v, tree[x].val);
} else return pre(tree[x].rs, v, ans);
}
}
1.3.5 后继
int nxt(int x, int v, int ans) {
if(v >= tree[x].val) {
if(tree[x].rs) {
return nxt(tree[x].rs, v, ans);
} else return ans;
} else {
if(!tree[x].ls) {
return (tree[x].val > v) ? tree[x].val : ans;
}
if(tree[x].cnt) {
return nxt(tree[x].ls, v, tree[x].val);
} else return nxt(tree[x].ls, v, ans);
}
}
以上为远古代码,码风奇丑轻喷
不难发现,如果数据为顺序插入一条链,那么搜索树也是一条链,这样单次操作的复杂度会升到 \(O(n)\) 级别。所以,平衡树诞生了!平衡树便是用来控制树高在 \(\log n\) 级别左右并且本身为一棵二叉搜索树的数据结构。
2. fhp-treap
2.1 简介
由范浩强巨佬发明的一棵无旋 treap。
2.1.1 何为 treap
\(treap = tree + Heap\)
考虑如何将一棵普通二叉搜索树维护平衡。
很显然,如果每次数据的插入数据随机,那么时间复杂度就能获得均摊的机会。比如按顺序插入 1 2 3 4 5
,同样的数据按 3 1 5 2 4
随机插入,树高就变底了,时间复杂度更为优秀。
但是我们无法做到将数据离线下来打乱随机插入,因为可能在期间会涉及到其他的一系列操作,使得最后得到的平衡树和某一时间戳内的不一致。
这时候范浩强巨佬便想到引入索引 key,使得平衡树最后按 key 为大根堆,按 val 为二叉搜索树。在 key 随机的时候,原有的二叉搜索树便能维持一个相对平衡的状态。
如何在二产搜索树的基础上维护堆呢?树旋转!当然我不会啊,于是乎范浩强巨佬又想出了无旋 treap 的写法(%%%。
2.1.2 有无旋转对 treap 的影响
可以这样讲,在我看来:二者常数都一般,无旋的常数要大,但是比有旋好写 N 倍。
所以赛场上的我会坚定不移的敲无旋而不是选择难调且耗时的有旋。
2.2 理论
fhq-treap 维护平衡无非就两种操作:分裂(split)与合并(merge)
- 分裂:分为按值分与按大小分。
按值分通常当做普通平衡树使用。而按大小分在文艺平衡树中常见。
这里讲解按值分:给定值 val,将一棵 treap 分裂为两棵树,第一棵树上的所有节点的值 \(\leq val\),第二棵树上的所有节点的值 \(> val\),且两树均为 treap。
例如,一棵很随意的树:
按 \(val=9\) 分裂,得到的结果如下图:
第一棵子树的根节点为黄色 \(x\),第二棵子树的根节点为蓝色 \(y\)。
考虑当前节点 \(p\) 在哪一个树,若 \(p\) 的 \(val\) 小于等于给定 \(val\),说明 \(p\) 及其左子树全部在 \(x\) 树内,然后分裂 \(p\) 的右子树,并寻找 \(p\) 的右子树内合适的 \(p\) 的新右儿子,若 \(p\) 的 \(val\) 大于给定 \(val\),则反之。
- 合并:将两 fhq-treap 树合并,前提满足第一棵树的所有值小于第二棵树的所有值。
若第一棵树的当前节点 \(x\) 的 \(key\) 大于第二棵树的当前节点 \(y\) 的 \(key\)。则 \(y\) 在 \(x\) 的右下方(默认 \(x\) 的 \(val\) 小于 \(y\) 的 \(val\))。然后合并 \(x\) 的右子树与 \(y\)。
若第一棵树的当前节点 \(x\) 的 \(key\) 小于等于第二棵树的当前节点 \(y\) 的 \(key\)。则 \(y\) 在 \(x\) 的右上方。合并 \(y\) 的左子树与 \(x\)。
在合并与分裂时应及时更新子树信息。(pushup)
2.3 实现
2.3.1 分裂 & 合并
void split(int p, int val, int &x, int &y) {
if(!p) {
x = y = 0;
return ;
}
if(t[p].val <= val) {
x = p;
split(t[p].r, val, t[p].r, y);
} else {
y = p;
split(t[p].l, val, x, t[p].l);
}
pushup(p);
}
int merge(int x, int y) {
if(!x || !y) return x + y;
if(t[x].key > t[y].key) {
t[x].r = merge(t[x].r, y);
pushup(x);
return x;
} else {
t[y].l = merge(x, t[y].l);
pushup(y);
return y;
}
}
2.3.2 插入
考虑新值 \(val\) 会出现在树的那一个位置。可以先将整棵树按照 \(val\) 分裂得到 \(x\),\(y\) 两树,在按顺序依次将 \(x\),新节点,\(y\) 的顺序合并即可(一定要按顺序,因为合并要保证 \(val_x\leq val < val_y\) )
void Ins(int val) {
split(root, val, x, y);
root = merge(merge(x, Mknode(val)), y);
}
2.3.3 删除
将树按照 \(val\) 分为 \(x, z\) 两树,在将 \(x\) 树按 \(val-1\) 分为 \(x, y\) 两树。因为 \(x\leq val\) 同时分为 \(x<val\) 和 \(y=val\),所以删除的值全在 \(y\) 树内,删一个就行,就将 \(y\) 根的左右儿子合并即可(相当于抛弃根节点)。
裂完记得并回来。
void Del(int val) {
split(root, val, x, z);
split(x, val-1, x, y);
y = merge(t[y].l, t[y].r);
root = merge(merge(x, y), z) ;
}
2.3.4 按值查排名
排名的定义是比 \(x\) 小的数的个数加一。
考虑将树按 \(val-1\) 分裂,\(x\) 树的大小加一即为答案。
裂完记得并回来。
int rk(int val) {
split(root, val-1, x, y);
int ans = t[x].siz + 1;
root = merge(x, y);
return ans;
}
2.3.5 按排名查值
平衡树上二分。
若左儿子的子树大小大于等于当前排名,则值在左子树;#若右儿子的子树大小小于当前排名,则值在右子树,排名更新为当前排名-左子树-1。
所以看得出本操作不基于 fhq-treap 的分裂与合并
int kth(int x) {
int p = root;
while(p) {
if(t[t[p].l].siz + 1 == x) return t[p].val;
else if(t[t[p].l].siz >= x) p = t[p].l;
else x -= (t[t[p].l].siz + 1), p = t[p].r;
}
}
2.3.6 前驱
前驱的定义为小于 \(x\) 中的最大的数。
将树分裂按 \(val-1\) 分裂,在 \(x\) 树中不停地跳右子树,直到不能跳了为止(无右儿子节点)。此点值即为答案。
int pre(int val) {
split(root, val-1, x, y);
int p = x;
while(t[p].r) p = t[p].r;
int ans = t[p].val;
root = merge(x, y);
return ans;
}
2.3.7 后继
后继的定义为大于 \(x\) 中的最小的数。
将树分裂按 \(val\) 分裂,在 \(y\) 树中不停地跳左子树,直到不能跳了为止(无左儿子节点)。此点值即为答案。
int pre(int val) {
split(root, val-1, x, y);
int p = x;
while(t[p].r) p = t[p].r;
int ans = t[p].val;
root = merge(x, y);
return ans;
}
2.3.8 fhq-treap 模版
#include <bits/stdc++.h>
#define ll long long
#define H 19260817
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define MOD 1000003
#define mod 1000000007
using namespace std;
inline int read() {
rint x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
void print(int x){
if(x<0){putchar('-');x=-x;}
if(x>9){print(x/10);putchar(x%10+'0');}
else putchar(x+'0');
return;
}
const int N = 1e5 + 10;
struct Node {
int l, r, val, key, siz;
} t[N];
int n, cnt, root;
mt19937 rd(114514);
int Mknode(int val) {//建立新节点
t[++cnt].val = val;
t[cnt].key = rd();
t[cnt].siz = 1;
return cnt;
}
void pushup(int p) {//上传信息
t[p].siz = t[t[p].l].siz + t[t[p].r].siz + 1;
return ;
}
void split(int p, int val, int &x, int &y) {
if(!p) {
x = y = 0;
return ;
}
if(t[p].val <= val) {
x = p;
split(t[p].r, val, t[p].r, y);
} else {
y = p;
split(t[p].l, val, x, t[p].l);
}
pushup(p);
}
int merge(int x, int y) {
if(!x || !y) return x + y;
if(t[x].key > t[y].key) {
t[x].r = merge(t[x].r, y);
pushup(x);
return x;
} else {
t[y].l = merge(x, t[y].l);
pushup(y);
return y;
}
}
int x, y, z;
void Ins(int val) {
split(root, val, x, y);
root = merge(merge(x, Mknode(val)), y);
}
void Del(int val) {
split(root, val, x, z);
split(x, val-1, x, y);
y = merge(t[y].l, t[y].r);
root = merge(merge(x, y), z) ;
}
int rk(int val) {
split(root, val-1, x, y);
int ans = t[x].siz + 1;
root = merge(x, y);
return ans;
}
int kth(int x) {
int p = root;
while(p) {
if(t[t[p].l].siz + 1 == x) return t[p].val;
else if(t[t[p].l].siz >= x) p = t[p].l;
else x -= (t[t[p].l].siz + 1), p = t[p].r;
}
}
int pre(int val) {
split(root, val-1, x, y);
int p = x;
while(t[p].r) p = t[p].r;
int ans = t[p].val;
root = merge(x, y);
return ans;
}
int nxt(int val) {
split(root, val, x, y);
int p = y;
while(t[p].l) p = t[p].l;
int ans = t[p].val;
root = merge(x, y);
return ans;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
while(n--) {
int op, x; cin >> op >> x;
if(op == 1) Ins(x);
else if(op == 2) Del(x);
else if(op == 3) cout << rk(x) << '\n';
else if(op == 4) cout << kth(x) << '\n';
else if(op == 5) cout << kth(rk(x) - 1) << '\n';
else cout << kth(rk(x + 1)) << '\n';
}
return 0;
}
拿走不谢!