各类平衡树的学习(二)——Treap

一.一道模板题.
题目:BZOJ3224.
题目大意:在序列上维护一个数据结构支持:
1.插入数 x x x.
2.删除数 x x x(只删一个).
3.查找 x x x的最小排名.
4.查找排名为 x x x的数.
5.查找 x x x的前驱.
6.查找 x x x的后继.
1 ≤ n ≤ 1 0 5 1\leq n\leq 10^5 1n105.

各类平衡树的学习(一)——Splay中,我们用splay解决了这个问题,接下来我们将会用另一种平衡树数据结构Treap解决这个问题.

建议先去看各类平衡树的学习(一)——二叉查找树BST.


二.Treap维护的信息.

Treap与普通BST存的信息基本一致,只是多存了一个用于维护平衡的随机出来的点权 v v v.

代码如下:

struct tree{
  int s[2],x,v,cnt,siz;
}tr[N+9];
int cn,rot;



三.Treap的性质.

Treap一个节点上存了两个附加值,一个是 x x x,我们称之为键值;一个是 v v v,我们称之为点权.其中点权是随机出来的.

与其他平衡树一样,Treap上键值满足BST性质;而与其他平衡树不同的是,Treap还需要使得点权满足堆性质.

为什么还要维护一个点权满足堆性质呢?因为键值是给定的,它可以特意构造数据把BST卡成链;而我们再随机出来的一个点权需要满足堆性质,既不会与原来键值满足BST性质产生冲突,也可以使得这棵BST的深度变得随机不会被卡了.

实际上,随机出来的Treap的深度期望为 O ( log ⁡ n ) O(\log n) O(logn),实践中一般在 log ⁡ n \log n logn 1.5 1.5 1.5 2 2 2倍左右,所以Treap就可以放心大胆的用了.


四.Treap的旋转.

虽然外面上面说了点权 v v v要满足堆性质就可以使Treap变得平衡,但是该怎么样在点权 v v v破坏了堆性质时,通过一些方式使这个破坏性质的节点满足堆性质呢?

当然是通过旋转.

我们分别称左旋 k k k z i g ( k ) zig(k) zig(k)和右旋 k k k z a g ( k ) zag(k) zag(k),形式分别如下:
在这里插入图片描述
在这里插入图片描述
分别可以写成:

void Zig(int &k){
  int s=tr[k].s[0];
  tr[k].s[0]=tr[s].s[1];tr[s].s[1]=k;
  k=s;
  Pushup(tr[k].s[1]);Pushup(k);
}

void Zag(int &k){
  int s=tr[k].s[1];
  tr[k].s[1]=tr[s].s[0];tr[s].s[0]=k;
  k=s;
  Pushup(tr[k].s[0]);Pushup(k);
}

当然也可以合并成一个函数:

void Rotate(int &k,int d){      //d为0左旋,为1右旋
  int s=tr[k].s[d];
  tr[k].s[d]=tr[s].s[d^1];tr[s].s[d^1]=k;k=s;
  Pushup(tr[k].s[d^1]);Pushup(k);
}



五.Treap的插入及建树.

Treap主要只有插入和删除是与普通BST不一样的,其它都是完全相同的代码,所以接下来只会介绍插入和删除.

插入只需要从根开始,大力往下找位置,如果找到相同权值的节点 k k k直接给 t r [ k ] . c n t tr[k].cnt tr[k].cnt 1 1 1并返回;否则会到一个空节点,然后新建一个节点赋一个随机点权,然后返回.退出每一层递归的过程中注意要判定这一层是否满足堆性质,不满足需要一次旋转.

代码如下:

void Pushup(int k){tr[k].siz=tr[tr[k].s[0]].siz+tr[tr[k].s[1]].siz+tr[k].cnt;}

int New_node(int x){
  tr[++cn]=tree();
  tr[cn].x=x;tr[cn].v=rand()%2333333+1;      //注意不能生成0
  tr[cn].cnt=tr[cn].siz=1;
  return cn;
}

void Insert(int x,int &k=rot){
  if (!k) {k=New_node(x);return;}
  if (tr[k].x==x) {++tr[k].cnt;Pushup(k);return;}
  Insert(x,tr[k].s[x>tr[k].x]);
  if (tr[k].v<tr[tr[k].s[x>tr[k].x]].v) Rotate(k,x>tr[k].x);
  Pushup(k);
}

接下来是Treap的建树,第一个节点的插入可以直接生成,但第二个节点的插入可能需要一次旋转,所以我们还是安安心心写一个插入好了.

代码如下:

void Build(){cn=0;rot=New_node(-INF);Insert(INF);}



六.Treap的删除.

回忆一下BST的删除,我们发现如果那样写又臭又长,所以我们考虑如何利用旋转这一条件.

类似于二叉堆的删除,我们直接把要删除的那个节点旋转到叶子直接删除即可.

代码如下:

void Erase(int x,int &k=rot){
  if (!k) return;
  if (x==tr[k].x){
  	--tr[k].cnt;Pushup(k);
  	if (tr[k].cnt<0) tr[k].cnt=0;
  	if (tr[k].cnt) return;
  	if (tr[k].s[0]||tr[k].s[1]){
	  if (!tr[k].s[0]||tr[tr[k].s[0]].v<tr[tr[k].s[1]].v)
	    Rotate(k,1),Erase(x,tr[k].s[0]);
	  else Rotate(k,0),Erase(x,tr[k].s[1]);
	}else k=0;
	return;
  }
  Erase(x,tr[k].s[x>tr[k].x]);
  Pushup(k);
}



七.随机数生成的常数优化.

我们知道C++内置的随机数生成函数 r a n d ( ) rand() rand()是非常慢且容易生成 0 0 0,所以考虑优化随机数生成的常数问题.

考虑随便选一个种子 s e e d seed seed,每次乘以一个大数并且对一个数取模后加 1 1 1避免出现 0 0 0.

代码如下:

int Rand(){return (sed=sed*19491001)%998244353+1;}



八.完整代码.

普通平衡树代码如下:

#include<bits/stdc++.h>
  using namespace std;

#define Abigail inline void
typedef long long LL;

const int N=100000,INF=(1<<31)-1;

struct tree{
  int s[2],x,v,cnt,siz;
}tr[N+9];
int cn,rot;
unsigned int sed=2333;

void Pushup(int k){tr[k].siz=tr[tr[k].s[0]].siz+tr[tr[k].s[1]].siz+tr[k].cnt;}
int Rand(){return (sed=sed*19491001)%998244353+1;}

void Rotate(int &k,int d){      //d为0左旋,为1右旋
  int s=tr[k].s[d];
  tr[k].s[d]=tr[s].s[d^1];tr[s].s[d^1]=k;k=s;
  Pushup(tr[k].s[d^1]);Pushup(k);
}

int Find_val(int x){
  int k=rot;
  for (;k&&tr[k].x^x;k=tr[k].s[x>tr[k].x]);
  return k?k:-1;
}

int New_node(int x){
  tr[++cn]=tree();
  tr[cn].x=x;tr[cn].v=Rand();
  tr[cn].cnt=tr[cn].siz=1;
  return cn;
}

void Insert(int x,int &k=rot){
  if (!k) {k=New_node(x);return;}
  if (tr[k].x==x) {++tr[k].cnt;Pushup(k);return;}
  Insert(x,tr[k].s[x>tr[k].x]);
  if (tr[k].v<tr[tr[k].s[x>tr[k].x]].v) Rotate(k,x>tr[k].x);
  Pushup(k);
}

void Build(){cn=0;rot=New_node(-INF);Insert(INF);}

int Query_rank(int x){
  int res=0,k=rot;
  for (;k&&tr[k].x^x;k=tr[k].s[x>tr[k].x])
    if (x>tr[k].x) res+=tr[tr[k].s[0]].siz+tr[k].cnt;
  if (k) res+=tr[tr[k].s[0]].siz;
  return res;
}

int Find_rank(int p){
  ++p;
  int k=rot;
  while (2333)
    if (p>tr[tr[k].s[0]].siz+tr[k].cnt) p-=tr[tr[k].s[0]].siz+tr[k].cnt,k=tr[k].s[1];
    else if (p<=tr[tr[k].s[0]].siz) k=tr[k].s[0];
      else return k;
}

int Lower(int x){
  int res=1,k=rot;
  for (;k&&tr[k].x^x;k=tr[k].s[x>tr[k].x])
    if (tr[k].x<x&&tr[k].x>tr[res].x) res=k;
  for (k=tr[k].s[0];k;k=tr[k].s[1])
    if (tr[k].x<x&&tr[k].x>tr[res].x) res=k;
  return res;
}

int Upper(int x){
  int res=2,k=rot;
  for (;k&&tr[k].x^x;k=tr[k].s[x>tr[k].x])
    if (tr[k].x>x&&tr[k].x<tr[res].x) res=k;
  for (k=tr[k].s[1];k;k=tr[k].s[0])
    if (tr[k].x>x&&tr[k].x<tr[res].x) res=k;
  return res; 
}

void Erase(int x,int &k=rot){
  if (!k) return;
  if (x==tr[k].x){
  	--tr[k].cnt;Pushup(k);
  	if (tr[k].cnt<0) tr[k].cnt=0;
  	if (tr[k].cnt) return;
  	if (tr[k].s[0]||tr[k].s[1]){
	  if (!tr[k].s[0]||tr[tr[k].s[0]].v<tr[tr[k].s[1]].v)
	    Rotate(k,1),Erase(x,tr[k].s[0]);
	  else Rotate(k,0),Erase(x,tr[k].s[1]);
	}else k=0;
	return;
  }
  Erase(x,tr[k].s[x>tr[k].x]);
  Pushup(k);
}

Abigail getans(){
  int opt,x,n;
  scanf("%d",&n);
  Build();
  while (n--){
  	scanf("%d%d",&opt,&x);
  	switch (opt){
  	  case 1:
  	  	Insert(x);
  	  	break;
  	  case 2:
  	    Erase(x);
  	  	break;
  	  case 3:
  	  	printf("%d\n",Query_rank(x));
  	  	break;
  	  case 4:
  	  	printf("%d\n",tr[Find_rank(x)].x);
  	  	break;
  	  case 5:
  	  	printf("%d\n",tr[Lower(x)].x);
  	  	break;
  	  case 6:
  	    printf("%d\n",tr[Upper(x)].x);
  	  	break;
  	}
  }
}

int main(){
  getans();
  return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值