平衡树学习笔记(5)-------SBT
SBT
所谓SBT,就是Size Balanced Tree
它的速度很快,完全碾爆Treap,Splay等平衡树,而且代码简洁易懂
尤其是插入节点多的时候,比其它树快多了(不考虑毒瘤红黑树)
尤其是它的平衡操作maintain,均摊\(O(1)\)!!!!
他maintain跟Splay差不多,都是依靠旋转来平衡
不过他可不想splay那样直接转到根,而是有条件的旋转

拿上图来说,SBT对于每个点,有两个平衡条件,假设说当前点是A,那么要满足以下两个条件,A才平衡
1、\(siz(B)\ge siz(G),siz(F)\)
2、\(siz(C)\ge siz(D),siz(E)\)
\(\color{#9900ff}{定义}\)
struct node {
	node *ch[2];
	int val, siz;
	node(int val = 0, int siz = 0): val(val), siz(siz) { ch[0] = ch[1] = NULL; }  //构造函数
	void upd() { siz = ch[0]->siz + ch[1]->siz + 1; }   //维护siz
	int rk() { return ch[0]->siz + 1; }       //获取当前排名
}*root, *null, pool[maxn], *tail, *st[maxn];  // 根,哨兵,内存池,当前指针,回收池
定义根Splay差不多,只是不用记录父亲
这里可以建立一个哨兵null,判断siz的时候比较好写
\(\color{#9900ff}{基本操作}\)
1、rotate
这个是旋转,跟Splay差不多
但是SBT是不用记录父亲的,所以要简单一点
这里有2个参数rotate(x,k)
意为把x向它的孩子k方向转
即为把x的另一个孩子!k转上来

void rot(node *&x, int k) {  //注意这里要取地址,maintain的时候也要取地址,目的是让x维护的是位置,而不是特定的点
	node *w = x->ch[!k];   //w就是上图的L2,即旋转中特殊的那个点
	x->ch[!k] = w->ch[k], w->ch[k] = x, w->siz = x->siz;
	x->upd(), x = w;   //让x仍然维护原来的位置
}
2、maintain
这是SBT维护平衡的操作函数
它基于1.rotate进行各种旋转
虽然看起来复杂度有点高
其实均摊可以达到\(O(1)\)!(虽然我不会证)
维护平衡一定要考虑全面,一旦露了某个子树,可能就会被卡QWQ
首先,先声明一点,maintain只有插入才会用到
因为删除不会增加复杂度,其它操作,树的形态又没变
因此,用到maintain的唯有插入
插入怎么插呢?
我们采用的是递归插入(必须递归插入,因为沿途每个点siz一变大,就可能不平衡!)
沿途siz++
那么,很显然可以得到从它到根的一条链
由于沿途siz++,所以
途中的这些点都有可能需要平衡
maintain(x,k)代表平衡x,至于k,左右哪边重,k就是哪边
下面我们开始分情况讨论一下maintain操作
我们发现,俩条件是对称的!下面我们只讨论一个条件,另一个就是反过来而已
Case 1: siz(A) > siz(R)

这时候我们把L转上来,就成了这样

但是o可能还是不平衡,比如上图,右边偏重,所以我们要继续maintain
Case 2: siz(B) >siz(R)

这时,我们把B直接转到o的位置上,即B左旋再右旋
然后,树就变成了这个样子

这时候会有好多地方不平衡qwq,但是可以发现,AEFR这些子树只是换了个位置,没有变,依然满足性质
因此我们调用两次maintain来平衡L和o
这个时候,除了B,其它所有点的子树都OK了,就剩下B了,它可能不满足1或2,所以再maintain(B)一下
代码实现的时候,可以记录一下哪边重,然后就可以写到一起,很简洁,好记
void maintain(node *&o, int k) {  //平衡o,哪边重,k就是哪边
	if(o->ch[k]->ch[k]->siz > o->ch[!k]->siz) rot(o, !k);  //情况1,
	else if(o->ch[k]->ch[!k]->siz > o->ch[!k]->siz) rot(o->ch[k], k), rot(o, !k); //情况2
	else return;
	maintain(o->ch[0], 0), maintain(o->ch[1], 1), maintain(o, 0), maintain(o, 1); //统一平衡,即使有些情况不涉及,反正递归下去也是直接return
}
\(e\color{#9900ff}{其它操作}\)
1、插入
关于插入,上面提了几句
总的来说就是从根出发,往孩子跳
这是一个递归的过程,必须递归,因为沿途回溯的时候要maintain,当然手动模拟递归也不拦你
void ins(node *&o, int val) {
	if(o == null) return (void)(o = newnode(val));  //到空节点,直接开新节点就行
	o->siz++;                                       //沿途siz++
	if(val <= o->val) ins(o->ch[0], val);           //找到插入位置
	else ins(o->ch[1], val);
	maintain(o, val > o->val);                      //每个点的子树进行平衡,在哪边插的,哪边可能就会变重,所以要平衡那一边
}
2、删除
因为它并没有Splay那样直接转到根这样的条件
它的旋转只是基于siz来的
所以,我们需要一些特别的方式
首先我们找到要删除的那个点,如果发现那个点并不是左右儿子都有,那就把那个儿子接上来就行了
否则,它一定存在左右儿子,也就是说它有后继,我们可以直接暴力找到它的后继,然后点权赋过来,改成在右边删除它的后继
见代码
void del(node *&o, int val) {
    //只要没找到,就递归删
	if(o->val != val) return (void)(del(o->ch[val > o->val], val), o->upd());
    //删除同理,沿途siz--
	o->siz--;
    //定义一个临时变量p为当前节点
	node *p = o;
    //如果x有一个孩子是空的,那么就简单了
    //把他不空的孩子接上来代替他的位置
    //显然不会影响平衡树的性质
    //这时只需把p删了即可
	if(o->ch[0] == null) o = o->ch[1], st[++top] = p;
	else if(o->ch[1] == null) o = o->ch[0], st[++top] = p;
	else {
        //这就是没有空孩子的情况
		p = o->ch[1];
		while(p->ch[0] != null) p = p->ch[0];
        //让p为o的后继(暴力找)
        //除了siz之外,二者全部交换
        //虽然o的值变成了p的
        //但是p的值没有改变!!!
        //因此,我们相当于已经用p代替了o
        //而且,因为p是o的后继,所以接上一定是满足平衡树性质的!
        //所以,我们的目标就只有一个了,那就是删掉p
        //而p作为后继,一定在o的右子树内
        //而且通过刚刚找后继的方式来看,它的左儿子一定是空的
        //所以当前位置的递归最多只会进入一次
        //这样也保证了复杂度!
		o->val = p->val, del(o->ch[1], p->val);
	}
}
上面一定要理解透彻
3、查询数x的排名
剩下的就差不多了qwq
int rnk(int val) {
	node *o = root; int rank = 0;
	while(o != null) {
		if(o->val < val) rank += o->rk(), o = o->ch[1];
		else o = o->ch[0];
	}
	return rank + 1;
}
4、查询第k大的数
int kth(int k) {
	node *o = root;
	while(o->rk() != k) {
		if(k > o->rk()) k -= o->rk(), o = o->ch[1];
		else o = o->ch[0];
	}
	return o->val;
}
5,6、前驱,后继
跟Splay的一毛一样qwq
毕竟都是平衡树,多少有点通性
int pre(int val) {
	node *o = root, *lst = null;
	while(o != null) {
		if(o->val < val) lst = o, o = o->ch[1];
		else o = o->ch[0];
	}
	return lst->val;
}
int nxt(int val) {
	node *o = root, *lst = null;
	while(o != null) {
		if(o->val > val) lst = o, o = o->ch[0];
		else o = o->ch[1];
	}
	return lst->val;
}
最后,放下完整代码
#include<bits/stdc++.h>
#define LL long long
LL in() {
	char ch; LL x = 0, f = 1;
	while(!isdigit(ch = getchar()))(ch == '-') && (f = -f);
	for(x = ch ^ 48; isdigit(ch = getchar()); x = (x << 1) + (x << 3) + (ch ^ 48));
	return x * f;
}
const int maxn = 1e5 + 10;
struct SBT {
	protected:
		struct node {
			node *ch[2];
			int val, siz;
			node(int val = 0, int siz = 0): val(val), siz(siz) { ch[0] = ch[1] = NULL; }
			void upd() { siz = ch[0]->siz + ch[1]->siz + 1; }
			int rk() { return ch[0]->siz + 1; }
		}*root, *null, pool[maxn], *tail, *st[maxn];
		int top;
		node *newnode(int val) {
			node *o = new(top? st[top--] : tail++) node(val, 1);
			return o->ch[0] = o->ch[1] = null, o;
		}
		void rot(node *&x, int k) {
			node *w = x->ch[!k];
			x->ch[!k] = w->ch[k], w->ch[k] = x, w->siz = x->siz;
			x->upd(), x = w;
		}
		void maintain(node *&o, int k) {
			if(o->ch[k]->ch[k]->siz > o->ch[!k]->siz) rot(o, !k);
			else if(o->ch[k]->ch[!k]->siz > o->ch[!k]->siz) rot(o->ch[k], k), rot(o, !k);
			else return;
			maintain(o->ch[0], 0), maintain(o->ch[1], 1), maintain(o, 0), maintain(o, 1);
		}
		void ins(node *&o, int val) {
			if(o == null) return (void)(o = newnode(val));
			o->siz++;
			if(val <= o->val) ins(o->ch[0], val);
			else ins(o->ch[1], val);
			maintain(o, val > o->val);
		}
		void del(node *&o, int val) {
			if(o->val != val) return (void)(del(o->ch[val > o->val], val), o->upd());
			o->siz--;
			node *p = o;
			if(o->ch[0] == null) o = o->ch[1], st[++top] = p;
			else if(o->ch[1] == null) o = o->ch[0], st[++top] = p;
			else {
				p = o->ch[1];
				while(p->ch[0] != null) p = p->ch[0];
				o->val = p->val, del(o->ch[1], p->val);
			}
		}
	public:
		SBT() {
			tail = pool; top = 0;
			root = null = new node();
			null->ch[0] = null->ch[1] = null;
		}
		void ins(int val) { ins(root, val); }
		void del(int val) { del(root, val); }
		int rnk(int val) {
			node *o = root; int rank = 0;
			while(o != null) {
				if(o->val < val) rank += o->rk(), o = o->ch[1];
				else o = o->ch[0];
			}
			return rank + 1;
		}
		int kth(int k) {
			node *o = root;
			while(o->rk() != k) {
				if(k > o->rk()) k -= o->rk(), o = o->ch[1];
				else o = o->ch[0];
			}
			return o->val;
		}
		int pre(int val) {
			node *o = root, *lst = null;
			while(o != null) {
				if(o->val < val) lst = o, o = o->ch[1];
				else o = o->ch[0];
			}
			return lst->val;
		}
		int nxt(int val) {
			node *o = root, *lst = null;
			while(o != null) {
				if(o->val > val) lst = o, o = o->ch[0];
				else o = o->ch[1];
			}
			return lst->val;
		}
}s;
int main() {
	for(int T = in(); T --> 0;) {
		int p = in();
		if(p == 1) s.ins(in());
		if(p == 2) s.del(in());
		if(p == 3) printf("%d\n", s.rnk(in()));
		if(p == 4) printf("%d\n", s.kth(in()));
		if(p == 5) printf("%d\n", s.pre(in()));
		if(p == 6) printf("%d\n", s.nxt(in()));
	}
	return 0;
}
												
											平衡树学习笔记(5)-------SBT的更多相关文章
- 平衡树学习笔记(6)-------RBT
		
RBT 上一篇:平衡树学习笔记(5)-------SBT RBT是...是一棵恐怖的树 有多恐怖? 平衡树中最快的♂ 不到200ms的优势,连权值线段树都无法匹敌 但是,通过大量百度,发现RBT的代码 ...
 - 平衡树学习笔记(3)-------Splay
		
Splay 上一篇:平衡树学习笔记(2)-------Treap Splay是一个实用而且灵活性很强的平衡树 效率上也比较客观,但是一定要一次性写对 debug可能不是那么容易 Splay作为平衡树, ...
 - 平衡树学习笔记(2)-------Treap
		
Treap 上一篇:平衡树学习笔记(1)-------简介 Treap是一个玄学的平衡树 为什么说它玄学呢? 还记得上一节说过每个平衡树都有自己的平衡方式吗? 没错,它平衡的方式是......rand ...
 - BST,Splay平衡树学习笔记
		
BST,Splay平衡树学习笔记 1.二叉查找树BST BST是一种二叉树形结构,其特点就在于:每一个非叶子结点的值都大于他的左子树中的任意一个值,并都小于他的右子树中的任意一个值. 2.BST的用处 ...
 - 普通平衡树学习笔记之Splay算法
		
前言 今天不容易有一天的自由学习时间,当然要用来"学习".在此记录一下今天学到的最基础的平衡树. 定义 平衡树是二叉搜索树和堆合并构成的数据结构,它是一 棵空树或它的左右两个子树的 ...
 - 浅谈树套树(线段树套平衡树)&学习笔记
		
0XFF 前言 *如果本文有不好的地方,请在下方评论区提出,Qiuly感激不尽! 0X1F 这个东西有啥用? 树套树------线段树套平衡树,可以用于解决待修改区间\(K\)大的问题,当然也可以用 ...
 - [学习笔记]平衡树(Splay)——旋转的灵魂舞蹈家
		
1.简介 首先要知道什么是二叉查找树. 这是一棵二叉树,每个节点最多有一个左儿子,一个右儿子. 它能支持查找功能. 具体来说,每个儿子有一个权值,保证一个节点的左儿子权值小于这个节点,右儿子权值大于这 ...
 - [学习笔记]可持久化数据结构——数组、并查集、平衡树、Trie树
		
可持久化:支持查询历史版本和在历史版本上修改 可持久化数组 主席树做即可. [模板]可持久化数组(可持久化线段树/平衡树) 可持久化并查集 可持久化并查集 主席树做即可. 要按秩合并.(路径压缩每次建 ...
 - 平衡树splay学习笔记#2
		
讲一下另外的所有操作(指的是普通平衡树中的其他操作) 前一篇的学习笔记连接:[传送门],结尾会带上完整的代码. 操作1,pushup操作 之前学习过线段树,都知道子节点的信息需要更新到父亲节点上. 因 ...
 
随机推荐
- 映射块设备提示rbd: sysfs write failed的解决方法
			
标签(空格分隔): ceph ceph运维 rbd 问题描述: 创建完镜像foo后,映射镜像时提示下面的错误: [root@node3 ~]# rbd map testpool/foo rbd: sy ...
 - UE4异步载入资源
			
转自:http://blog.ch-wind.com/ue4%E5%BC%82%E6%AD%A5%E8%BD%BD%E5%85%A5%E8%B5%84%E6%BA%90/ 所有的“硬”指针指向的资源都 ...
 - kubernetes 学习   创建cronjob
			
POM.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="htt ...
 - CentOS7 线上环境的一些 配置
			
上周服务器被攻击导致上面收回了我们服务器的IP,所以这周重新安装部署了服务器,使用centos7系统.为了防止服务器再次被攻击,所以建议以下几点: 1. root密码要复杂一点,尽量字母数字特殊字符都 ...
 - 监控和安全运维  1.4 nagios安装
			
1. Nagios 简介是一个开源软件,可以监控网络设备网络流量.Linux/windows主机状态,甚至可以监控打印机它可以运行在Linux上或windows上基于浏览器的web界面方便运维人员查看 ...
 - C语言学习笔记--条件编译
			
C语言中的条件编译的行为类似于 C 语言中的 if…else…,是预编译指示命令,用于控制是否编译某段代码 . 1.条件编译的本质 (1)预编译器根据条件编译指令有选择的删除代码 (2)编译器不知道代 ...
 - js实现鼠标拖拽
			
主要原理: 1.当鼠标按下时,记录鼠标坐标,用到的是 onmousedown: 2.当鼠标移动时,计算鼠标移动的坐标之差,用到的是 onmousemove: 3.当鼠标松开时,清除事件,用到的是 on ...
 - 关于Bundle对象的思考
			
在开发过程中,我们经常使用bundle对象来携带二进制数据,通过INTENT传递出去,那么BUNDLE对象到底是什么?其结构如何? 简要来说,bundle对象类似于一个map,内部是通过<key ...
 - 关于handler和异步任务
			
handler使用流程概要 首先在主线程新建一个handler实例,重写onhandlemessage(Message msg) 方法,对传过来的message进行处理 然后在子线程中完成操作,操作完 ...
 - Nginx 正向代理和反向代理
			
正向代理的概念 正向代理,也就是传说中的代理,他的工作原理就像一个跳板,简单的说,我是一个用户,我访问不了某网站,但是我能访问一个代理服务器这个代理服务器呢,他能访问那个我不能访问的网站于是我先连上代 ...