[BST]BST笑传之拆查补(替罪羊树)

替罪羊树

前言

首先我们要知道 BST 是什么

其实就是一颗用来查找的二叉树,然后有个特性,在任意子树,它的左节点权值小于根节点权值小于右节点权值

那么我们知道如果这个树是完全对称的,他的查找复杂度就是 \(O(log N)\) 的,然而要把它一直维持在完美的平衡状态很不容易

这就催生了一系列的 BST , 如 Treap , AVL , LCT 等

替罪羊树则是 BST 中最易于理解的一种,它不需要任何高端操作。

正篇

替罪羊树的思想非常暴力,就是设置两个子树的大小比例来保证查找效率,如果一个分支过大或过小,就把整个子树拆了重建

重建操作

那么怎么重建呢?就是先保存一下需要拆除的子树的节点的中序遍历,再在保证上文提到的特性的情况下,用中序遍历建一颗完美新子树

大概就是这样
L

看过一个很形象的描述就是中序遍历之后“拎起来”。

不平衡率

还有一个核心的点就是上文提到过的设置两子树的比例,那么设置一个定值 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 合集中的第一篇 :替罪羊树解析

posted @ 2025-03-23 17:38  風月華  阅读(117)  评论(0)    收藏  举报