【学习笔记】平衡树

介绍

平衡树是一种特殊的二叉树搜树,他能在被修改后,依靠分裂,合并,等操作使得树能始终保持平衡(每一个节点的两棵子树的大小尽量相等),这里主要讲解 FHQtreap。

操作

FHQtreap 也叫无旋 treap,他的每个节点有两个值 val,pri, 其中 pri 满足二叉堆的性质,而 val 满足 BST 的性质。注意与 Splay 不同,对于 val 相同的点 FHQtreap 会开不同的点进行维护。

  • 更新 pushup
    这是最基本的操作。每一个节点的大小等于他的两个儿子的大小加 1
void pushup(int u){
	t[u].siz = t[t[u].ls].siz + t[t[u].rs].siz + 1;
} 
  • 创建新节点 add
    更新 cnt,size,val,pri 即可。
int add(int val){
	t[++cnt].pri = rand(), t[cnt].siz = 1, t[cnt].val = val;
	return cnt;
} 
  • 分裂 split(按 val)
    这是平衡树中最重要的操作。
    对于分裂完后的两棵树 L,R 需要满足 L 中的点的 valxR 中的点的 val>x
    函数中的 &L,&R 分别表示左树,右树中的接口,即新的节点可以接到哪里。
    若当前节点的 valx 则他的左儿子一定属于左树,所以往右递归,而左树新的接口就变为了他的右儿子(左儿子没有改变,并且他要满足 BST 的性质)。
    若当前节点的 val>x 则他的右儿子一定属于右树,所以往左递归,而右树新的接口就变为了他的左儿子(右儿子没有改变,并且他要满足 BST 的性质)。
void split(int u, int x, int &L, int &R){
	if(!u) return L = 0, R = 0, void();
	if(t[u].val <= x) L = u, split(t[u].rs, x, t[u].rs, R);
	else R = u, split(t[u].ls, x, L, t[u].ls);
	pushup(u);
}
  • 合并 merge
    若当前左树顶点的 pri 更小,则要把左树顶点的右儿子,改为将他原先右儿子与右树合并后的新顶点(BST 性质)。
    若当前右树顶点的 pri 更小,则要把右树顶点的左儿子,改为将他原先左儿子与左树合并后的新顶点(BST 性质)。
int merge(int L, int R){
	if(!L || !R) return L | R;
	if(t[L].pri <= t[R].pri) return t[L].rs = merge(t[L].rs, R), pushup(L), L;
	else return t[R].ls = merge(L, t[R].ls), pushup(R), R;
}
  • 添加 Insert
    将树按权值为 val(当前点的值)裂开成两棵树,然后进行两次合并。
void Insert(int val){
	int x = add(val), L, R;
	split(rt, val, L, R);
	rt = merge(merge(L, x), R);
} 
  • 删除 Delete
    将树分别按权值为 val,val1 裂开为三棵树 L,P,R,然后再将左树,t[p].lst[p].rs,右树,合并
void Delete(int x){
	int L, P, R;
	split(rt, x, L, R);
	split(L, x - 1, L, P);
	rt = merge(merge(L, merge(t[P].ls, t[P].rs)), R);
}
  • 排名 getrank
    将树按 val1 为权值进行分裂,答案就是左树的 siz+1
int getrank(int val){
	int L, R, ans;
	split(rt, val - 1, L, R);
	ans = t[L].siz + 1;
	rt = merge(L, R);
	return ans;
}
  • k 大的树 kth
    按左右树的 siz DFS 即可。
int Kth(int u, int k){
	int x = t[t[u].ls].siz + 1;
	if(x == k) return t[u].val;
	if(k > x) return Kth(t[u].rs, k - x);
	else return Kth(t[u].ls, k);
}
  • 前驱 pre 后继 suc
    先计算排名,然后再调用 kth 函数即可。
    注意前驱是 getrank(x)-1 而后继是 getrank(x+1)
int pre(int x){
	return Kth(rt, getrank(x) - 1);
}
int suc(int x){
	return Kth(rt, getrank(x + 1));
}

例题 文艺平衡树

构造一个数列 a,b,c,d,e,假设一棵树的中序遍历就是这个数列,容易发现若将数列中的某个区间翻转,就是将树中的某个子树的左右儿子全变翻转。
由此我们可以构造一棵平衡树,他的每个节点的 val 就是数列的下标 ipri还是随机的。
对于每次区间操作可以将树按 siz 进行分裂,注意不能按 val 进行分裂,因为翻转后的树的 val 是不满足 BST 的性质的。

split(rt, r, L, R);
split(L, l - 1, L, P);

由于每次都对全部的儿子进行翻转是很费时间的,所以可以引入一个类似线段树懒标记 lazytag 的东西。在每次操作的时候进行下传即可。

  • pushdown
    连续两次翻转相当于不翻转。
void pushdown(int u){
	swap(t[u].ls, t[u].rs);
	t[t[u].ls].lazy ^= 1;
	t[t[u].rs].lazy ^= 1;
	t[u].lazy = 0;
}
  • split
void split(int u, int x, int &L, int &R){
	if(!u) return L = 0, R = 0, void();
	if(t[u].lazy) pushdown(u);
	if(t[t[u].ls].siz + 1 <= x){
		L = u, split(t[u].rs, x - t[t[u].ls].siz - 1, t[u].rs, R);
	}
	else{
		R = u, split(t[u].ls, x, L, t[u].ls);
	}
	pushup(u);
}
  • merge
int merge(int L, int R){
	if(!L || !R) return L | R;
	if(t[L].pri < t[R].pri){
		if(t[L].lazy) pushdown(L);
		t[L].rs = merge(t[L].rs, R);
		return pushup(L), L;
	}
	else{
		if(t[R].lazy) pushdown(R);
		t[R].ls = merge(L, t[R].ls);
		return pushup(R), R;
	}
}
posted @   GuoSN0410  阅读(10)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
· Manus的开源复刻OpenManus初探
点击右上角即可分享
微信分享提示