记得有一天翔哥毒奶我们:

当你们已经在平衡树的海洋中畅游时,我还在线段树的泥沼中挣扎。

我觉得其实像我这种对平衡树一无所知的蒟蒻也要开一开数据结构了。

然后花了一天啃了下最简单的平衡树Treap,感觉还是可以接受的。

下一步争取把Splay和替罪羊树给nāng下来。至于SB-tree还是算了吧。

首先我们要知道Treap为什么要叫Treap,这很简单:

Treap=Tree+heap(树+堆)

我们类比于一般的BST,它们都有一些共同的特点:

  1. 都是二叉树的结构
  2. 对于所有的非叶子节点,它的左儿子(如果有的话)的值都严格小于它,它的右儿子(如果有的话)的值都严格大于它。

很显然,期望情况下树的高度就是\(log\)级别的了。然后一次操作的复杂度就是\(O(log\ n)\)的

然后对于普通的BST,就有一个致命的弱点了。让数据为一条链时,那么树的高度就变成\(n\)的了。

所以我们就要让树的高度尽量平衡,于是我们想当:当数据随机排列是这个数高就是\(log\)级别的了

但是数据是不可能重新排列的,但我们可以手动调整树的形状,并且只要在基于随机的基础上就可以了。然后就有了Treap。

Treap的基本思想也建立于此,我们在刚开始给每个节点随机一个值。然后对于这棵树在保持BST性质的同时还要保持堆的性质,这里我一般采用大根堆的性质。

然后就有一个问题了。我们按BST的性质插入后,如何维护堆的性质?

然后就是整个Treap中(也是Splay中)最难的操作了——旋转

我们先放一张经典的图:

所谓左旋就是逆时针,右旋为顺时针。向上面那样,旋转之后,节点的左右位置不变,即BST性质不变,而p与k的上下位置变化了,a和b的深度也发生变化了。通过旋转,我们也可以在不破坏BST性质的前提下维护堆性质

然后我们就只要在插入和删除时通过旋转维护堆性质即可。

然后我们结合一道模板题P3369 【模板】普通平衡树(Treap/SBT)来具体讲解一下Treap的操作(代码有注释)

#include<cstdio>
using namespace std;
const int N=100005,INF=1e9;
struct treap
{
int val,dat,size,cnt,ch[2];
}node[N];//Treap的节点信息
//val表示数的值,dat表示rand()出来的优先级,size是子树大小,cnt是一个点的重复的个数,ch[]表示的是左右儿子的编号,由于没有用指针,请大家注意一下下面的很多操作都是要加'&'的
int m,opt,x,rt,tot;
inline char tc(void)
{
static char fl[100000],*A=fl,*B=fl;
return A==B&&(B=(A=fl)+fread(fl,1,100000,stdin),A==B)?EOF:*A++;
}
inline void read(int &x)
{
x=0; char ch=tc(); int flag=1;
while (ch<'0'||ch>'9') { if (ch=='-') flag=-1; ch=tc(); }
while (ch>='0'&&ch<='9') x=x*10+ch-'0',ch=tc(); x*=flag;
}
inline void write(int x)
{
if (x<0) putchar('-'),x=-x;
if (x>9) write(x/10);
putchar(x%10+'0');
}
inline int rand() //加速的手写rand()函数,那个玄学的取值我也不知道是什么鬼。
{
static int seed=233;
return seed=(int)seed*482711LL%2147483647;
}
inline void pushup(int rt)//类似于线段树的更新,注意不要忘记把自身的副本数加上
{
node[rt].size=node[node[rt].ch[0]].size+node[node[rt].ch[1]].size+node[rt].cnt;
}
inline int build(int v)//建立新的节点,返回节点编号
{
node[++tot].val=v; node[tot].dat=rand();
node[tot].size=node[tot].cnt=1; return tot;
}
inline void init(void)//初始化,防止后面的一些操作出现越界错误(不用指针就是有些问题)
{
rt=build(-INF); node[rt].ch[1]=build(INF); pushup(rt);
}
inline void rotate(int &rt,int d)//旋转,0表示左旋,1表示右旋
{
int temp=node[rt].ch[d^1]; node[rt].ch[d^1]=node[temp].ch[d]; node[temp].ch[d]=rt; //这里注意修改的顺序,强烈建议自己手动画图理解一下
rt=temp; pushup(node[rt].ch[d]); pushup(rt);
}
inline void insert(int &rt,int v)//插入一个值为v的数
{
if (!rt) { rt=build(v); return; }
if (v==node[rt].val) ++node[rt].cnt; else //注意重复的节点处理方法
{
int d=v<node[rt].val?0:1; insert(node[rt].ch[d],v);//SBT的性质
if (node[node[rt].ch[d]].dat>node[rt].dat) rotate(rt,d^1);//如果违反就旋转,注意这里的d要^1(结合旋转方向理解一下即可)
}
pushup(rt);
}
inline void remove(int &rt,int v)
{
if (!rt) return;
if (v==node[rt].val)
{
if (node[rt].cnt>1) { --node[rt].cnt; pushup(rt); return; }
if (node[rt].ch[0]||node[rt].ch[1])//如果这个节点有子树就要旋到叶节点再删除
{
if (!node[rt].ch[1]||node[node[rt].ch[0]].dat>node[node[rt].ch[1]].dat) rotate(rt,1),remove(node[rt].ch[1],v);
else rotate(rt,0),remove(node[rt].ch[0],v); pushup(rt);
} else rt=0; return;//这里是对于所有删除情况的结束
}
if (v<node[rt].val) remove(node[rt].ch[0],v); else remove(node[rt].ch[1],v); pushup(rt);//同样要符合SBT的性质
}
inline int get_rank(int &rt,int v)//查询v的排名
{
if (!rt) return 0;
if (v==node[rt].val) return node[node[rt].ch[0]].size+1; else//相等就返回子树大小+1
if (v<node[rt].val) return get_rank(node[rt].ch[0],v); else//小于就在左子树中找
return node[node[rt].ch[0]].size+node[rt].cnt+get_rank(node[rt].ch[1],v);//大于在右子树中找,注意这里要减去那一部分
}
inline int get_val(int &rt,int rk)//查询排名为rk的数
{
if (!rt) return INF;//道理基本同上
if (rk<=node[node[rt].ch[0]].size) return get_val(node[rt].ch[0],rk); else
if (rk<=node[node[rt].ch[0]].size+node[rt].cnt) return node[rt].val; else //注意这里不要忽略副本的多少
return get_val(node[rt].ch[1],rk-node[node[rt].ch[0]].size-node[rt].cnt);
}
inline int get_pre(int &rt,int v)//找前驱
{
int now=rt,pre;
while (now)//个人感觉迭代的比递归好些
{
if (node[now].val<v) pre=node[now].val,now=node[now].ch[1];//这里要注意,如果当前点的值小于目标那么总是到右子树去找(有点贪心的思想)
else now=node[now].ch[0];
}
return pre;
}
inline int get_next(int &rt,int v)//找后继,原理同上
{
int now=rt,next;
while (now)
{
if (node[now].val>v) next=node[now].val,now=node[now].ch[0];
else now=node[now].ch[1];
}
return next;
}
int main()
{
//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
read(m); init();
while (m--)
{
read(opt); read(x);
switch (opt)
{
case 1:insert(rt,x); break;
case 2:remove(rt,x); break;
case 3:write(get_rank(rt,x)-1),putchar('\n'); break; //注意这里由于我们刚开始是加入了两个放溢出的节点,因此要-1。下面同理
case 4:write(get_val(rt,x+1)),putchar('\n'); break;
case 5:write(get_pre(rt,x)),putchar('\n'); break;
case 6:write(get_next(rt,x)),putchar('\n'); break;
}
}
return 0;
}

其中Treap还有类似于堆的功能,可以求出Treap中的最大(小)值

具体实现很简单,由于SBT的性质,所以我们一直从左子树(找最小值时)或右子树(找最大值时)一路找到叶子节点即可,这里我们结合一道板子题POJ3481来看一下吧

CODE

#include<cstdio>
using namespace std;
const int N=1e6+5;
struct Treap
{
int val,dat,num,ch[2];
}node[N];
int opt,k,x,rt,tot,now,size;
inline char tc(void)
{
static char fl[100000],*A=fl,*B=fl;
return A==B&&(B=(A=fl)+fread(fl,1,100000,stdin),A==B)?EOF:*A++;
}
inline void read(int &x)
{
x=0; char ch=tc(); int flag=1;
while (ch<'0'||ch>'9') { if (ch=='-') flag=-1; ch=tc(); }
while (ch>='0'&&ch<='9') x=x*10+ch-'0',ch=tc(); x*=flag;
}
inline void write(int x)
{
if (x<0) putchar('-'),x=-x;
if (x>9) write(x/10);
putchar(x%10+'0');
}
inline int rand(void)
{
static int seed=233;
return seed=(int)seed*482711LL%2147483647;
}
inline int build(int v,int num)
{
node[++tot].val=v; node[tot].dat=rand(); node[tot].num=num; return tot;
}
inline void rotate(int &rt,int d)
{
int temp=node[rt].ch[d^1]; node[rt].ch[d^1]=node[temp].ch[d];
node[temp].ch[d]=rt; rt=temp;
}
inline void insert(int &rt,int v,int num)
{
if (!rt) { rt=build(v,num); return; }
int d=v<node[rt].val?0:1; insert(node[rt].ch[d],v,num);
if (node[rt].dat<node[node[rt].ch[d]].dat) rotate(rt,d^1);
}
inline void remove(int &rt,int v)
{
if (!size) return;
if (v==node[rt].val)
{
if (node[rt].ch[0]||node[rt].ch[1])
{
if (!node[rt].ch[1]||node[node[rt].ch[0]].dat>node[node[rt].ch[1]].dat) rotate(rt,1),remove(node[rt].ch[1],v);
else rotate(rt,0),remove(node[rt].ch[0],v);
} else rt=0; return;
}
if (v<node[rt].val) remove(node[rt].ch[0],v); else remove(node[rt].ch[1],v);
}
inline int get_min(int rt)
{
if (!size) return 0;
while (node[rt].ch[0]) rt=node[rt].ch[0];
now=node[rt].val; return node[rt].num;
}
inline int get_max(int rt)
{
if (!size) return 0;
while (node[rt].ch[1]) rt=node[rt].ch[1];
now=node[rt].val; return node[rt].num;
}
int main()
{
//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
for (;;)
{
read(opt); if (!opt) break;
switch (opt)
{
case 1:read(k),read(x),insert(rt,x,k),++size; break;
case 2:write(get_max(rt)),putchar('\n'),size&&(remove(rt,now),--size); break;
case 3:write(get_min(rt)),putchar('\n'),size&&(remove(rt,now),--size); break;
}
}
return 0;
}

在平衡树的海洋中畅游(一)——Treap的更多相关文章

  1. 在平衡树的海洋中畅游(四)——FHQ Treap

    Preface 关于那些比较基础的平衡树我想我之前已经介绍的已经挺多了. 但是像Treap,Splay这样的旋转平衡树码亮太大,而像替罪羊树这样的重量平衡树却没有什么实际意义. 然而类似于SBT,AV ...

  2. 在平衡树的海洋中畅游(二)——Scapegoat Tree

    在平衡树的广阔天地中,以Treap,Splay等为代表的通过旋转来维护平衡的文艺平衡树占了觉大部分. 然而,今天我们要讲的Scapegoat Tree(替罪羊树)就是一个特立独行的平衡树,它通过暴力重 ...

  3. 在平衡树的海洋中畅游(三)——Splay

    Preface 由于我怕学习了Splay之后不直接写blog第二天就忘了,所以强行加了一波优先级. 论谁是天下最秀平衡树,我Splay第一个不服.维护平衡只靠旋转. 一言不合转死你 由于平衡树我也介绍 ...

  4. 【BZOJ5020】【THUWC2017】在美妙的数学王国中畅游(Link-Cut Tree,组合数学)

    [BZOJ5020][THUWC2017]在美妙的数学王国中畅游(Link-Cut Tree,组合数学) 题解 Description 数字和数学规律主宰着这个世界. 机器的运转, 生命的消长, 宇宙 ...

  5. [THUWC2017]在美妙的数学王国中畅游

    [THUWC2017]在美妙的数学王国中畅游 e和sin信息不能直接合并 泰勒展开,大于21次太小,认为是0,保留前21次多项式即可 然后就把e,sin ,kx+b都变成多项式了,pushup合并 上 ...

  6. [BZOJ5020][THUWC2017]在美妙的数学王国中畅游(LCT)

    5020: [THUWC 2017]在美妙的数学王国中畅游 Time Limit: 80 Sec  Memory Limit: 512 MBSec  Special JudgeSubmit: 323  ...

  7. 【BZOJ5020】[THUWC 2017]在美妙的数学王国中畅游 泰勒展开+LCT

    [BZOJ5020][THUWC 2017]在美妙的数学王国中畅游 Description 数字和数学规律主宰着这个世界. 机器的运转, 生命的消长, 宇宙的进程, 这些神秘而又美妙的过程无不可以用数 ...

  8. 平衡树学习笔记(2)-------Treap

    Treap 上一篇:平衡树学习笔记(1)-------简介 Treap是一个玄学的平衡树 为什么说它玄学呢? 还记得上一节说过每个平衡树都有自己的平衡方式吗? 没错,它平衡的方式是......rand ...

  9. 普通平衡树Tyvj1728、luogu P3369 (treap)

    您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作: 插入x数 删除x数(若有多个相同的数,因只删除一个) 查询x数的排名(若有多个相同的数,因输出最小的排名) 查询排名为x的 ...

随机推荐

  1. recovery log直接输出到串口

    我们在调试recovery升级的时候,我们经常需要查看recovery的log,google的原始逻辑中,recovery的log并非直接输出到串口,我们需要输入命令才能获取,我们有三种方式: 第一种 ...

  2. 跨站请求伪造(CSRF)

    1. 什么是跨站请求伪造(CSRF)  CSRF(Cross-site request forgery跨站请求伪造,也被称为“One Click Attack”或者session Riding,通常缩 ...

  3. Oracle EBS FA 资产取值

    SELECT fb.book_type_code, fth.ASSET_NUMBER, fdh.units_assigned, fdh.assigned_to, pf.FULL_NAME, fl.se ...

  4. 洗礼灵魂,修炼python(40)--面向对象编程(10)—定制魔法方法+time模块

    定制魔法方法 1.什么是定制魔法方法 首先定制是什么意思呢?其实就是自定义了,根据我们想要的要求来自定义.而在python中,其实那些所谓的内置函数,内置方法,内置属性之类的其实也是自定义出来的,不过 ...

  5. fedora更新

    先换源再更新,否则等的太久,如果已经开始了直接ctrl+c取消 # dnf update

  6. MySQL基本简单操作03

    MySQL基本简单操作 现在我创建了一个数据表,表的内容如下: mysql> select * from gubeiqing_table; +----------+-----+ | name | ...

  7. January 29th, 2018 Week 05th Monday

    Losing all hope was freedom. 彻底绝望就是真正的自由. Losing all the hopes, and we are free to challenge everyth ...

  8. 17秋 软件工程 团队第五次作业 Alpha Scrum3

    17秋 软件工程 团队第五次作业 Alpha Scrum3 今日完成的任务 杰麟:java后端学习: 世强:Android的部门基础信息模块的信息显示和对接后台: 港晨:后台管理登陆界面ui设计: 树 ...

  9. 使用ElasticSearch实现搜索时即时提示与全文搜索功能

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  10. 理解 JavaScript 中的 for…of 循环

    什么是 for…of 循环 for...of 语句创建一个循环来迭代可迭代的对象.在 ES6 中引入的 for...of 循环,以替代 for...in 和 forEach() ,并支持新的迭代协议. ...