码迷,mamicode.com
首页 > 其他好文 > 详细

浅谈替罪羊树

时间:2020-06-16 18:04:04      阅读:132      评论:0      收藏:0      [点我收藏+]

标签:lazy   重要   左右子树   loading   巩固   lin   节点   lse   不同   

替罪羊树 学习总结

前言:

为什么会学替罪羊树?因为觉得AVL树那些的左旋右旋什么的太晕了啊QAQ

所以就在RHL大佬的推荐下,学习起了替罪羊树,这种不用旋转操作就能维护平衡的树


知识介绍:

在OI界一直都会有这样的一句话:“暴力即优雅”,而诸如分块、替罪羊树则是对这句话的最好诠释

对于二叉搜索树,最重要的就是维护树的平衡,将时间复杂度保持在O(logN)左右,使其不会退化成一条链,从而到时时间复杂度增长到O(N)

  • 在替罪羊树上,插入或删除节点的平摊最坏时间复杂度是O(logN),搜索节点的最坏时间复杂度是 O(logN)

像AVL树、Splay一类的树都是通过旋转来维持平衡,而替罪羊树呢,则是简单粗暴的“不平衡?那就拍扁重建!

什么意思?让我们通过一些问答来理解一下

  • 什么时候拍扁重建?

在每次插入点后,判断当前子树是否平衡,如果不平衡则拍扁重建

  • 怎么判断是否平衡?

如果一棵树的左子树/右子树的存在的节点数量 > 这棵树的存在的节点数量× alpha,那么就要拍扁重建

  • alpha是什么?

alpha是我们人为选择的一个平衡因子,在0.5-1之间,一般选择0.7或0.8


基础操作:

在了解了替罪羊树的基础知识以后,让我们来学习一下替罪羊树的基本操作

  • 一些变量
intn,x,op,mep,tep,root,tmp[2000005],mem[2000005];

//tmp拍扁的时候用的内存空间
//mep指向内存池mem[]的指针
//tep指向拍扁时用的tmp[]的指针

struct node {
	int lc,rc,v,valid,total; //valid子树未被删除的点数,total子树总点数 
	bool pd;  //是否被删除:1表示未被删除,0表示被删除 
} a[2000005];

  • 判断是否拍扁重建
inline bool flag(int now) { //判断是否需要平衡一下 
	if((double)a[now].valid*alpha<=(double)max(a[a[now].lc].valid,a[a[now].rc].valid)) return true;
	return false;
}

  • 建树&调整整棵树
inline void build(int l,int r,int &now) { //建树&调整维护
	int mid=(l+r)>>1;
	now=tmp[mid]; //tmp里存的是编号:把中间的元素取出来,中间元素的编号为now
	if(l==r) {
		a[now].lc=a[now].rc=0; //新插入的节点都为叶子节点,进行初始化 
		a[now].total=a[now].valid=1;
		return;
	}
	if(l<mid) build(l,mid-1,a[now].lc); //mid已经建完了,建左右子树 
	else a[now].lc=0; //l==mid,则没有左儿子,但此时r那个节点作为了mid节点的右儿子 
	build(mid+1,r,a[now].rc);
	/* 因为mid总是(l+r)>>1向下取整,所以只需要判断l是小于mid还是等于mid,而ri永远大于mid */
	a[now].total=a[a[now].lc].total+a[a[now].rc].total+1; //更新节点信息 
	a[now].valid=a[a[now].lc].valid+a[a[now].rc].valid+1;
}

  • DFS求拍扁的顺序

我们来看一看替罪羊树是怎么拍扁需要重构的树的,如下草图:

技术图片

我们可以发现拍扁后的序列其实是已经排好序的,而这个顺序就是对这棵重建子树的中序遍历,所以我们重建前需要dfs一下

inline void dfs(int now) { //中序遍历(左根右),找出要被拍扁的节点的编号 
	if(!now) return; //叶子节点 
	dfs(a[now].lc);
	if(a[now].pd==1) tmp[++tep]=now; //加入到拍扁的时候用的数组里存放(pd是惰性删除) 
	else mem[++mep]=now;
	dfs(a[now].rc);
}

  • 重建
inline void rebuild(int &now) {
	tep=0; //重建的子树要从头开始算
	dfs(now); //dfs找到重建的顺序 
	if(tep) build(1,tep,now);
	else now=0;
}
  • 插入一个数

替罪羊树在插入时,是一边向下一边更新,这也是与其他树不同的地方

inline void insert(int &now,int k) {
	if(!now) { //找到一个插入的位置 
		now=mem[mep--];
		a[now].v=k;
		a[now].pd=a[now].total=a[now].valid=1;
		a[now].lc=a[now].rc=0;
		return;
	}
	a[now].total++; //一边向下一边更新
	a[now].valid++;
	if(a[now].v>=k) insert(a[now].lc,k);
	else insert(a[now].rc,k);
	if(flag(now)==true) rebuild(now); //从下往上重建会更快(因为下面的子树小,好操作) 
}
  • 查询数k的排名
inline int findth(int k) { //查找值为k的排名 
	int now=root;
	int ans=1;
	while(now) {
		if(a[now].v>=k) now=a[now].lc;
		else {
			ans+=a[a[now].lc].valid+a[now].pd; //+a[now].pd是因为相同大小的节点虽然放在一起,但是我不知道这个节点上相同的是不是还存在啊..所以得单独加该节点..至于valid是除现节点以外的子树大小。
			now=a[now].rc;
		}
	}
	return ans;
}
  • 查询排名为k的值
inline  int findn(int k) { //查找排名为k的值 
	int now=root;
	while(now) {
		if(a[now].pd&&a[a[now].lc].valid+1==k) return a[now].v;
		else if(a[a[now].lc].valid>=k) now=a[now].lc;
		else {
			k-=a[a[now].lc].valid+a[now].pd;
			now=a[now].rc;
		}
	}
}
  • 删除值为k的的数

这里是通过转换为:先求值k的排名,再删除排名为k的数

注意一下,这里的删除都是惰性删除,即给删除的点打上标记

真正的删除是在DFS那里进行的

同时,删除之后我们也要判断一下是否需要重建(这里的判断条件与之前有略微不同)

inline void deleth(int &now, int k) { //删除排名为k的数
	if(a[now].pd&&a[a[now].lc].valid+1==k) {
		a[now].pd=0;
		a[now].valid--;
		return;
	}
	a[now].valid--;
	if(a[a[now].lc].valid+a[now].pd>=k) deleth(a[now].lc,k); 
	else deleth(a[now].rc,k-a[a[now].lc].valid-a[now].pd);
}

inline void deletn(int k) { //删除值为k的数
	deleth(root, findth(k));
	if((double)a[root].total*alpha>a[root].valid) rebuild(root); //删太多也重建一下
}

例题:

(1)洛谷P3369 基础版普通平衡树

(2)洛谷P6136 加强版普通平衡树


完整代码:

现在来一发没有注释的Code

PS:以下给出的是例题1的代码(例题2的代码只需要在主程序上更改以下即可,文末会给出)

#include <bits/stdc++.h>
#define alp 0.8
using namespace std;
int n,x,op,mep,tep,root,tmp[2000005],mem[2000005];

struct node {
	int lc,rc,v,valid,total;
	bool pd;
} a[2000005];

inline bool flag(int now) {
	if((double)a[now].valid*alp<=(double)max(a[a[now].lc].valid,a[a[now].rc].valid)) return true;
	return false;
}

inline void build(int l,int r,int &now) {
	int mid=(l+r)>>1;
	now=tmp[mid];
	if(l==r) {
		a[now].lc=a[now].rc=0;
		a[now].valid=a[now].total=1;
		return ;
	}
	if(l<mid) build(l,mid-1,a[now].lc);
	else a[now].lc=0;
	build(mid+1,r,a[now].rc);
	a[now].total=a[a[now].lc].total+a[a[now].rc].total+1;
	a[now].valid=a[a[now].lc].valid+a[a[now].rc].valid+1;
}

inline void dfs(int now) {
	if(!now) return ;
	dfs(a[now].lc);
	if(a[now].pd==1) tmp[++tep]=now;
	else mem[++mep]=now;
	dfs(a[now].rc);
}

inline void rebuild(int &now) {
	tep=0;
	dfs(now);
	if(tep) build(1,tep,now);
	else now=0;
}

inline void insert(int &now,int k) {
	if(!now) {
		now=mem[mep--];
		a[now].v=k;
		a[now].lc=a[now].rc=0;
		a[now].pd=a[now].valid=a[now].total=1;
		return ;
	}
	a[now].total++;
	a[now].valid++;
	if(a[now].v>=k) insert(a[now].lc,k);
	else insert(a[now].rc,k);
	if(flag(now)==true) rebuild(now);
}

inline int findth(int k) {
	int now=root;
	int ans=1;
	while(now) {
		if(a[now].v>=k) now=a[now].lc;
		else {
			ans+=a[a[now].lc].valid+a[now].pd;
			now=a[now].rc;
		}
	}
	return ans;
} 

inline int findn(int k) {
	int now=root;
	while(now) {
		if(a[now].pd&&a[a[now].lc].valid+1==k) return a[now].v;
		else if(a[a[now].lc].valid>=k) now=a[now].lc;
		else {
			k-=a[now].pd+a[a[now].lc].valid;
			now=a[now].rc;
		}
	}
}

inline void deleth(int &now,int k) {
	if(a[now].pd&&a[a[now].lc].valid+1==k) {
		a[now].pd=0;
		a[now].valid--;
		return ;
	}
	a[now].valid--;
	if(a[a[now].lc].valid+a[now].pd>=k) deleth(a[now].lc,k);
	else deleth(a[now].rc,k-a[a[now].lc].valid-a[now].pd);
}

inline void deletn(int k) {
	deleth(root,findth(k));
	if((double)a[root].total*alp>a[root].valid) rebuild(root);
}

int main() {
	for(register int i=2000000;i>=1;i--) mem[++mep]=i;
	scanf("%d",&n);
	for(register int i=1;i<=n;i++) {
		scanf("%d%d",&op,&x);
		if(op==1) insert(root,x);
		if(op==2) deletn(x);
		if(op==3) printf("%d\n",findth(x));
		if(op==4) printf("%d\n",findn(x));
		if(op==5) printf("%d\n",findn(findth(x)-1));
		if(op==6) printf("%d\n",findn(findth(x+1)));
	}
	return 0;
}
  • 例题2的主程序部分(其他的函数部分和以上一致):
int main() {
	for(register int i=2000000;i>=1;i--) mem[++mep]=i;
	n=read();
	m=read();
	for(register int i=1;i<=n;i++) {
		x=read();
		insert(root,x);
	}
	for(register int i=1;i<=m;i++) {
		op=read();
		x=read();
		x^=last;
		if(op==1) insert(root,x);
		if(op==2) deletn(x);
		if(op==3) {
			ans^=findth(x);
			last=findth(x);
		}
		if(op==4) {
			ans^=findn(x);
			last=findn(x);
		}
		if(op==5) {
			ans^=findn(findth(x)-1);
			last=findn(findth(x)-1);
		}
		if(op==6) {
			ans^=findn(findth(x+1));
			last=findn(findth(x+1));
		}
	}
	printf("%d",ans);
	return 0;
}

后序:

终于总结完了!替罪羊树也算是入门了吧?

嗯..不过还需要多做题巩固应用,AVL树和Splay也得找时间学习

那....那就继续加油叭qvq

最后,如果有任何问题,欢迎指出,我们一起进步


浅谈替罪羊树

标签:lazy   重要   左右子树   loading   巩固   lin   节点   lse   不同   

原文地址:https://www.cnblogs.com/Eleven-Qian-Shan/p/13143290.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!