爆炸的平衡树, 替罪羊树

爆炸的平衡树, 替罪羊树

由于Defad不太喜欢旋转, 所以一般用替罪羊树. 这里写个博客介绍一下.

什么是二叉搜索树

可以维护一个集合, 相比于权值线段树 (动态开点) 的时间复杂度 \(\log{N}\) 空间复杂度 \(N \log{N}\), 二叉搜索树理论上来说只需要 \(\log{N}\) 的时间复杂度 (最坏是 \(N\)), 但是空间复杂度可以达到 \(N\).

这里不过多胡扯, 只能说普通二叉搜索树时间复杂度期望 \(\log{N}\) 最坏是 \(N\).

权值数据结构水各种题

什么是替罪羊树

考虑优化刚才的二叉搜索树, 可以带着标题里的 "爆炸" 进行考虑.

替罪羊树的想法是, 当有一个结点左子树和右子树差距过大, 可以炸掉这个结点的子树, 然后重新构造.

记录子树大小

我们每个结点用 \(3\) 个变量记录子树大小, 分别维护元素数, 未删除结点数, 总结点数.

这么说似乎有点抽象, 就是说,

  • \(sz0_{p}\) 维护这个子树里面有多少元素 (当然, 如果不是可重集就不用这个, 因为重复就是删除).
  • \(sz1_{p}\) 维护这个子树里有多少结点还没有被删除, 被删除的不计.
  • \(sz2_{p}\) 维护这个子树里面实际有多少结点, 被删除的也要记录.

结点 \(p\) 的值被删除时我们仅给了 \(cnt_{p} := \max(cnt_{p} - 1, 0)\), 炸子树时就无需打印了.

void push_up(int p){
	tr[p].sz0=tr[tr[p].ls].sz0+tr[tr[p].rs].sz0+tr[p].cnt;
	tr[p].sz1=tr[tr[p].ls].sz1+tr[tr[p].rs].sz1+(tr[p].cnt?1:0);
	tr[p].sz2=tr[tr[p].ls].sz2+tr[tr[p].rs].sz2+1;
}

爆炸

我们先不考虑怎么判断平衡, 考虑如何炸掉一个结点及其子树.

二叉搜索树的中序遍历是单调的, 那么我们可以打印中序遍历到一个数组里, 这里我们选择记录下标, 就不用记录值和次数, 然后申请很多结点去构造子树了, 只需要更改左右儿子指针和子树大小即可.

But how on earth are we gonna do that?

但我们究竟该怎么做呢?

Why don't you confer with Mr.Finnigan?

你为什么不和斐尼甘先生商量一下呢?

void squib(int p){
	if(p==0){
		return;
	}
	squib(tr[p].ls);
	if(tr[p].cnt){
		g[++cntg]=p;
	}
	squib(tr[p].rs);
}

这个函数在 debug 的时候可以直接炸掉根 \(rt\) 然后不重构, 然后挨个输出 \(i \in [1, cntg]\)\(val_{g_{i}}\).

void print(){
	squib(rt);
	f1(i,1,cntg,1){
		cout<<tr[g[i]].val<<" \n"[i==cntg];
	}
}

愣着干什么, 重构啊

毕竟都炸完了, 重构吧.

重构基本和线段树建树一样, 区别仅仅是当前结点是 \(mid\), 然后左子树只是 \([1, mid - 1]\) 了.

虽然我讲线段树也没说过建树, 当时说调用 \(N\) 次修改即可.

线段树, 算法竞赛掌管区间的神

我再说一遍, 如果我在参数里写了指针, 那么我的建议还是传引用, int &p 后面就不需要解引用了.

void build(int *p,int l,int r){
	if(l>r){
		*p=0;
		return;
	}
	if(l==r){
		tr[g[l]].ls=tr[g[l]].rs=0;
		push_up(g[l]);
		*p=g[l];
		return;
	}
	int m=l+r>>1;
	build(&tr[g[m]].ls,l,m-1);
	build(&tr[g[m]].rs,m+1,r);
	push_up(g[m]);
	*p=g[m];
}
void rebuild(int *p){
	cntg=0;
	squib(*p);
	build(p,1,cntg);
}

什么情况下就不够平衡, 需要重构呢?

首先, 每次插入元素和删除元素就有可能不平衡, 而查询 \(k\) th 和查询排名并不对树产生修改 (前驱后继都是用这个做的), 所以不可能需要重构.

考虑完什么时候有可能重构, 那么考虑在什么情况下重构.

替罪羊树考虑的是, 引入一个平衡因子 \(\alpha\), 在不满足 \(\alpha\) 的条件时重构子树.

又臭又长, 但是比旋转好记多了, 也不害怕写挂, 反正写挂最多写成普通二叉搜索树.

int check(int p){
	return tr[p].sz0&&(tr[p].sz2*alpha<=max(tr[tr[p].ls].sz2,tr[tr[p].rs].sz2)||alpha*tr[p].sz2>=tr[p].sz1);
}

然后在我们的插入元素和删除元素的递归的最后加上这个.

if(check(*p)){
	rebuild(p);
}

插入元素和删除元素

普通的二叉搜索树插入, 删除的时候给结点的 \(cnt_{p} := \max(cnt_{p} - 1, 0)\), 爆炸时如果 \(cnt_{p} = 0\) 则不打印.

ifelse ifelse 千万不能乱.

void push(int *p,int k){
	if(*p==0){
		*p=++cntt;
		tr[*p].val=k;
		tr[*p].cnt=1;
		push_up(*p);
		return;
	}
	else if(k==tr[*p].val){
		tr[*p].cnt++;
	}
	else if(k<tr[*p].val){
		push(&tr[*p].ls,k);
	}
	else{
		push(&tr[*p].rs,k);
	}
	push_up(*p);
	if(check(*p)){
		rebuild(p);
	}
}
void pop(int *p,int k){
	if(*p==0){
		return;
	}
	if(k<tr[*p].val){
		pop(&tr[*p].ls,k);
	}
	else if(k==tr[*p].val){
		tr[*p].cnt=max(tr[*p].cnt-1,0);
	}
	else{
		pop(&tr[*p].rs,k);
	}
	push_up(*p);
	if(check(*p)){
		rebuild(p);
	}
}

\(k\) th 和排名

类似线段树上二分, 这里用二叉搜索树上二分, 并不难.

需要注意的是 \(x\) 的排名是 rk(rt,x-1)+1 也就是最后一个比 \(x\) 小的元素的排名 \(+ 1\) 就是 \(x\) 的排名.

这么写表面是因为致敬权值线段树, 实际还是不习惯平衡树.

int kth(int p,int k){
	if(p==0){
		return -1;
	}
	else if(k<=tr[tr[p].ls].sz0){
		return kth(tr[p].ls,k);
	}
	else if(k<=tr[tr[p].ls].sz0+tr[p].cnt){
		return tr[p].val;
	}
	else{
		return kth(tr[p].rs,k-(tr[tr[p].ls].sz0+tr[p].cnt));
	}
}
int rk(int p,int k){
	if(p==0){
		return 0;
	}
	else if(k<tr[p].val){
		return rk(tr[p].ls,k);
	}
	else if(k==tr[p].val){
		return tr[tr[p].ls].sz0+tr[p].cnt;
	}
	else{
		return tr[tr[p].ls].sz0+tr[p].cnt+rk(tr[p].rs,k);
	}
}

前驱后继

不多说, 直接放代码, 理解起来很容易, 如果你用权值线段树水过平衡树板子.

kth(rt,rk(rt,x-1)) // 前驱
kth(rt,rk(rt,x)+1) // 后继

例题

平衡树

VJugde-LuoGu LuoGu

VJudge-DarkBZOJ DarkBZOJ

Tyvj 为什么找不到了 555

这里还是只给 main 函数.

read(&Q);
while(Q--){
	read(&op);
	if(op==1){
		read(&x);
		push(&rt,x);
	}
	else if(op==2){
		read(&x);
		pop(&rt,x);
	}
	else if(op==3){
		read(&x);
		cout<<rk(rt,x-1)+1<<endl;
	}
	else if(op==4){
		read(&x);
		cout<<kth(rt,x)<<endl;
	}
	else if(op==5){
		read(&x);
		cout<<kth(rt,rk(rt,x-1))<<endl;
	}
	else if(op==6){
		read(&x);
		cout<<kth(rt,rk(rt,x)+1)<<endl;
	}
	else{
		cout<<"_"<<endl;
	}
}
posted @ 2024-12-27 02:54  指针神教教主Defad  阅读(14)  评论(0编辑  收藏  举报