[BST]BST笑传之拆查补(替罪羊树)
替罪羊树
前言
首先我们要知道 BST 是什么
其实就是一颗用来查找的二叉树,然后有个特性,在任意子树,它的左节点权值小于根节点权值小于右节点权值
那么我们知道如果这个树是完全对称的,他的查找复杂度就是 \(O(log N)\) 的,然而要把它一直维持在完美的平衡状态很不容易
这就催生了一系列的 BST , 如 Treap , AVL , LCT 等
替罪羊树则是 BST 中最易于理解的一种,它不需要任何高端操作。
正篇
替罪羊树的思想非常暴力,就是设置两个子树的大小比例来保证查找效率,如果一个分支过大或过小,就把整个子树拆了重建
重建操作
那么怎么重建呢?就是先保存一下需要拆除的子树的节点的中序遍历,再在保证上文提到的特性的情况下,用中序遍历建一颗完美新子树
大概就是这样

看过一个很形象的描述就是中序遍历之后“拎起来”。
不平衡率
还有一个核心的点就是上文提到过的设置两子树的比例,那么设置一个定值 alpha 来确保任何一个子树的占比不超过alpha
否则重建。这个 alpha 经过实验设置成 0.7 左右最佳
插入与删除
插入是采取动态开点,没啥好说的
删除的话不可能删一个节点重建一次,不然那个 alpha 就没用了。所以我们选择在结构体里记录一下这个点存不存在,然后
在重建的时候在顺便给他删了。
代码
其实思路就是这么个思路,然而这个玩意相较于同是蓝题的主席树,代码是真长
我们可以做一下平衡树板子,有一些细节的问题放代码里了
代码参考 《算法竞赛》 4.12 节的替罪羊树板子
#include <bits/stdc++.h>
#define qwq return 0
#define int long long
const int N = 1e6 + 7;
const double alpha = 0.7;
namespace Sgt {
using namespace std;
stack<int> st; // 由于是动态开点所以节点可以重复利用,这个栈就是一个回收站的作用
struct Node {
int del , ls , rs , val , tot , siz; //tot是删掉的节点加上没删的总数,siz则是当前没删的节点总数
//del为0就是被删了,为1则是没有,这个设置有利于其他操作
} t[N];
int n , root , cnt;
int order[N]; // order,cnt就是重建的时候存中序遍历用的
inline bool not_balance(int s) {
if(t[s].siz * 1.0 * alpha <= (double) max(t[t[s].ls].siz , t[t[s].rs].siz)) {//任意一个子树过大或过小
return true;
}
else {
return false;
}
}
inline void update(int s) {//和线段树一样的更新
t[s].siz = t[t[s].ls].siz + t[t[s].rs].siz + 1;
t[s].tot = t[t[s].ls].tot + t[t[s].rs].tot + 1;
}
inline void Init(int u) {
t[u].ls = t[u].rs = 0;
t[u].siz = t[u].tot = t[u].del = 1;
}
inline void build(int l , int r , int &s) {
int mid = l + r >> 1;
s = order[mid]; // 让中点为根,构建出符合性质的完美子树
if(l == r) {
Init(s); return;
}
if(l < mid) {
build(l , mid - 1 , t[s].ls);
}
if(l == mid) { //出现有一些节点只有一个儿子的情况不要忘记把另一个设置成空
t[s].ls = 0;
}
build(mid + 1 , r , t[s].rs);
update(s);
}
inline void inorder(int s) {//把中序遍历序列整出来
if(!s) {
return;
}
inorder(t[s].ls);
if(t[s].del) {
order[++ cnt] = s;
}
else {
st.push(s);
}
inorder(t[s].rs);
}
inline void rebuild(int &s) {
cnt = 0;
inorder(s);
if(cnt) {
build(1 , cnt , s);
}
else {
s = 0;
}
}
inline void insert(int &s , int x) {
if(!s) {
s = st.top(); st.pop(); //动态开点
t[s].val = x;
Init(s); return;
}
++t[s].siz; ++t[s].tot;
if(t[s].val >= x) {//利用了BST的性质
insert(t[s].ls , x);
}
else {
insert(t[s].rs , x);
}
if(not_balance(s)) {
rebuild(s);//不平衡就拆掉
}
}
inline void del_k(int &s , int k) {
-- t[s].siz;
if(t[s].del && t[t[s].ls].siz + 1 == k) {
t[s].del = 0; return;
}
if(t[t[s].ls].siz + t[s].del >= k) {//这个有点像主席树的遍历思路
del_k(t[s].ls , k);
}
else {
del_k(t[s].rs , k - (t[t[s].ls].siz + t[s].del));
}
}
inline int rank(int s , int x) { //查询有多少权值严格小于x
if(!s) {
return 0;
}
if(x > t[s].val) {
return t[t[s].ls].siz + t[s].del + rank(t[s].rs , x);
}
else {
return rank(t[s].ls , x);
}
}
inline void del(int s) {//删节点
del_k(root , rank(root , s) + 1);//等于删是这个节点排名的节点
if(t[root].tot * alpha >= t[root].siz) {//删的太多才要拆掉重建
rebuild(root);
}
}
inline int kth(int k) {//查询第k大权值
int s = root;
while(s) {//这个实际上也可以递归实现(?
if(t[s].del && t[t[s].ls].siz + 1 == k) {//寻找一个左子树加上自己正好有k个节点的节点
return t[s].val;
}
else if(t[t[s].ls].siz >= k) {
s = t[s].ls;
}
else {
k -= t[t[s].ls].siz + t[s].del;
s = t[s].rs;
}
}
return t[s].val;
}
void sgt() {
ios :: sync_with_stdio(NULL) , cin.tie(NULL) , cout.tie(NULL);
for(register int i = N - 1; i >= 1; --i) {
st.push(i);
}
cin >> n;
while(n--) {
int opt , x; cin >> opt >> x;
switch(opt) {
case 1 : {
insert(root , x); break;
}
case 2 : {
del(x); break;
}
case 3 :{
cout << rank(root , x) + 1 << '\n'; break;
}
case 4 : {
cout << kth(x) << '\n'; break;
}
case 5 : {
cout << kth(rank(root , x)) << '\n'; break;//这两个思考一下也不难,前驱就是:权值中,排名是 权值严格小于x的节点数量 的节点权值
}
case 6 : {
cout << kth(rank(root , x + 1) + 1) << '\n'; break;//后驱自己领会,语言系统崩溃了
}
}
}
}
}
signed main() {
Sgt :: sgt();
qwq;
}
这是 BST 合集中的第一篇 :替罪羊树解析

浙公网安备 33010602011771号