【平衡树——非旋treap】笔记

数据结构可爱捏

1. 什么是非旋treap

非旋treap,本质上是一颗利用了随机数优化的二叉搜索树,可以通过合并(merge)操作和分离(split)操作的组合,实现点的添加与删除,查询排名、前驱、后继等操作,也可以实现很多线段树无法实现的区间操作,十分优秀,就是有点搞心态。(绝对不会告诉你我的饭卡套就是因此而牺牲的)

1.1. 为什么要随机数优化

显然,对于同样的一个集合,它可以生成出许多不同的二叉搜索树。而如果出题人是个毒瘤,那么,二叉搜索树会退化成链,时间复杂度为 O ( n ) O(n) O(n),而随机数优化后,它的高度的期望值为 log ⁡ n \log n logn,即时间复杂度直接降为 O ( log ⁡ n ) O(\log n) O(logn),相当优秀。

那么,随机数优化到底是怎样优化的?

显然,二叉搜索树的节点权值只有一种,而非旋treap的节点权值分为两种,一种是他自己所带的权值key,另一种则是随机数赋予它的rk

在创建非旋treap时,首先保证rk的值是小的在上,大的在下,在保证key值“左子树小于自己小于右子树”,这样一来,就相当将原数列随机打乱后再插入到一颗二叉搜索树中,就能实现随机数优化

简洁一点:玄学优化

2. merge操作

merge操作,就是将两颗非旋treap合并为一颗非旋treap

显然,考虑两颗树的根节点(假设分别是 a a a b b b):谁的 rk 值小,谁就成为新的非旋treap的跟(这里假设 a a a 成为了根),然后,比较key值,如果 b b b 的key值小于 a a a 的key值,则 b b b 这颗非旋treap再与 a a a 的左子树合并,否则, b b b 这颗非旋treap与 a a a 的右子树合并

代码:

int merge(int root1,int root2){
//这里保证了key[root1]<key[root2	]
	if(!root1||!root2){//只剩一棵非旋treap就可以停止了
		return root1+root2;
	}
	if(rk[root1]<rk[root2]){
		son[root1][1]=merge(son[root1][1],root2);
		return root1;
	}else{
		son[root2][0]=merge(root1,son[root2][0]);
		return root2;
	}
}

3. split操作

一般来说,split操作分为两种,一种是按照key值,一种是按照树的大小。这里以key值大小为例(假设为 y y y

显然,如果根节点 a a a 的key值小于 y y y,那么,节点 a a a 本身和它的左子树全部分成左边部分,然后只需要分 a a a 的右子树即可;反之,节点 a a a 本身和它的右子树全部分成左边部分,然后只需要分 a a a 的左子树即可

代码:


void split(int root,int x,int &a1,int &a2){
	if(!root){//分到尽头
		a1=a2=0;
		return ;
	}
	if(key[root]<=x){//两种情况
		split(son[root][1],x,son[root][1],a2);
		a1=root;
	}else{
		split(son[root][0],x,a1,son[root][0]);
		a2=root;
	}
}

基本上,非旋treap的核心就是这两个代码了,其它的操作基本上都是由这两个操作搭配而来

Eg 1.【模板】普通平衡树

解决这道题,我们还需要维护一个Size数组,Size数组可以在merge操作和split操作中更新

想要解决后三个操作,我们需要再写一个函数:Find(int now,int x),意思是在根节点为now的树中寻找第x号节点。显然,若now的左子树的Size小于等于 x x x,那就去左子树找;若now的左子树的Size再加一等于 x x x,那么答案就是now号节点;否则,就去右子树里面找

代码:

int find(int now,int k){
	while(1){
		if(Size[son[now][0]]>=k){
			now=son[now][0];
		}else if(Size[son[now][0]]+1==k){
			return now;
		}else{
			k-=(Size[son[now][0]]+1);//记得去右子树时更新k值
			now=son[now][1];
		}
	}
}

这样一来,这道题就可以迎刃而解了,代码:

#include<ctime>
#include<cstdio>
#include<cstdlib>
const int N=1000005;
int n;
int cnt,root;
int key[N],rk[N],son[N][2],Size[N];
void pushup(int x){
	Size[x]=Size[son[x][0]]+Size[son[x][1]]+1;
}
int merge(int root1,int root2){
	if(!root1||!root2){
		return root1+root2;
	}
	if(rk[root1]<rk[root2]){
		son[root1][1]=merge(son[root1][1],root2);
		pushup(root1);
		return root1;
	}else{
		son[root2][0]=merge(root1,son[root2][0]);
		pushup(root2);
		return root2;
	}
}
void split(int root,int x,int &a1,int &a2){
	if(!root){
		a1=a2=0;
		return ;
	}
	if(key[root]<=x){
		split(son[root][1],x,son[root][1],a2);
		a1=root;
	}else{
		split(son[root][0],x,a1,son[root][0]);
		a2=root;
	}
	pushup(root);
}
int make_new(int x){
	key[++cnt]=x,rk[cnt]=rand(),Size[cnt]=1;
	return cnt;
}
int add(int root,int x){//加入新结点
	int X,Y;
	split(root,x,X,Y);
	return merge(merge(X,make_new(x)),Y);
}
int del(int root,int x){//删掉节点
	int X,Y,Z;
	split(root,x,X,Y);
	split(X,x-1,X,Z);
	return merge(merge(X,merge(son[Z][0],son[Z][1])),Y);//注意,因为只要求删掉一个节点,如果直接将(X,Y)合并,会出问题
}
int find(int now,int k){
	while(1){
		if(Size[son[now][0]]>=k){
			now=son[now][0];
		}else if(Size[son[now][0]]+1==k){
			return now;
		}else{
			k-=(Size[son[now][0]]+1);
			now=son[now][1];
		}
	}
}
void solve_num_rank(int x){//根据数字查排名
	int X,Y;
	split(root,x-1,X,Y);
	printf("%d\n",Size[X]+1);//左子树节点个数加上它本身
	merge(X,Y);
}
void solve_rank_num(int x){//根据排名查数字
	printf("%d\n",key[find(root,x)]);
}
void solve_pre(int x){//查询前驱
	int X,Y;
	split(root,x-1,X,Y);
	printf("%d\n",key[find(X,Size[X])]);//左子树的最后一个
	merge(X,Y);
}
void solve_next(int x){//查询后继
	int X,Y;
	split(root,x,X,Y);
	printf("%d\n",key[find(Y,1)]);//右子树的第一个
	merge(X,Y);
}
int main(){
	srand(time(0));
	scanf("%d",&n);
	while(n--){
		int op,x;
		scanf("%d%d",&op,&x);
		if(op==1){
			root=add(root,x);
		}else if(op==2){
			root=del(root,x);
		}else if(op==3){
			solve_num_rank(x);
		}else if(op==4){
			solve_rank_num(x);
		}else if(op==5){
			solve_pre(x);
		}else{
			solve_next(x);
		}
	}
	return 0;
}

这里随便举几个例题吧:

Eg 2.营业额统计

显然,只需要按顺序插入每一个数,然后查询它的前驱以及后继即可,注意判断这个数是否有前驱、后继

Eg 3.报表统计

MIN_SORT_GAP:显然,每插入一个数,他都会和它的前驱以及后继各产生一个差值,所以,我们每往treap1插入一个数,就分别用它的前驱以及它的后继与该数字本身各产生一个差,用它们来更新最小值,再在该操作中输出这个最小值即可

INSERT:假设我们插入的数字是 x x x,它前面的数是 a a a,后面的数是 b b b

显然,插入后,原本的 a − b a-b ab 就没了,而变为了 x − a x-a xa b − x b-x bx,分别将它们删除,加入treap2即可

注意, b b b 有可能是不存在的,注意特判

MIN_GAP:输出treap2的第一位即可

Eg 4.宠物收养场

这道题可以建两颗treap,但是我只建了一颗,因为显然,当宠物大于人时,treap里面装的时宠物的信息,反之,装的是人的信息

这里以宠物信息为例:

每有一个人加入时,查询前驱和后继,哪个与之的差小(差一样就选择前驱),就将答案累加给它,然后删除对应的点即可

Eg 5.郁闷的出纳员

我们还需要两个变量:sum,minn,分别表示员工工资的起伏和假设员工工资不变,工资下限的相对起伏(有点抽象,但应该能理解)

为了解决这道题,我们还需要两个变量,分别表示公司里的人数和离开公司的人数

I:先判断这个员工的工资是否小于工资下限,如果不小于,就将该员工加入即可

A:调整summinn

S:调整summinn,同时,判断目前公司工资最小的员工是否跳槽,持续跳槽,直到无人跳槽(即查询排名第一小的员工)

F:找到排名对应的数,注意加上sum(特判:公司人数可能不足 k k k

最后输出跳槽人数即可

还在咕咕咕呢喵

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值