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

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

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

然后花了一天啃了下最简单的平衡树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. 安卓开发_关于WebView使用链接时调用浏览器显示的问题

    在我们的实际开发中,我们用到WebView就是为了在自己的APP中的某个部分来显示指定网页的效果. 但是在学习的过程中,我发现一个问题: 有的网页使用WebView控件显示出来以后,再点击网页中的某个 ...

  2. 针对模拟滚动条插件(jQuery.slimscroll.js)的修改

    在开发过程中程序员总会碰到产品经理提出的各种稀奇古怪的需求,尽管有些需求很奇葩,但不得不说有些须有还是能指引我们不断的学习与进步,最近在工作中就碰到这种问题.需求是要求在各主流浏览器上使用自定义的滚动 ...

  3. 【锁】Oracle死锁(DeadLock)的分类及其模拟

    [锁]Oracle死锁(DeadLock)的分类及其模拟 1  BLOG文档结构图 2  前言部分 2.1  导读和注意事项 各位技术爱好者,看完本文后,你可以掌握如下的技能,也可以学到一些其它你所不 ...

  4. 利用Spring的AbstractRoutingDataSource解决多数据源的问题

    多数据源问题很常见,例如读写分离数据库配置. 原来的项目出现了新需求,局方要求新增某服务器用以提供某代码,涉及到多数据源的问题. 解决方法如下: 1.首先配置多个datasource <bean ...

  5. 位运算符&与、或|、异或^

    &按照二进制位进行运算 如:运算规则:0&0=0: 0&1=0:1&0=0:1&1=1:即:两位同时为“1”,结果才为“1”,否则为0[有0则0] 3& ...

  6. linux下安装mysql简单步骤

    linux下使用yum安装mysql 1.安装 查看有没有安装过: yum list installed mysql* rpm -qa | grep mysql* 查看有没有安装包: yum list ...

  7. gnome extensions 推荐 (fedora 28 常用gnome 插件备份)

    当我们进行重新安装系统(fedora 28)的时候,需要初始安装一些 gnome 插件,来进行完善我们的使用. 首先我们应该进行安装 gnome-tweak 工具来进行定制化系统. tweak 可以进 ...

  8. My strength (C-A-R)

    My strength: I am good at problem resolving Challenge In the first year when I come to America I pas ...

  9. cobaltstrike3.8服务器搭建及使用

    参考链接: https://www.ezreal.net/archives/166.htmlhttp://blog.cobaltstrike.com/category/cobalt-strike-2/ ...

  10. java集合类List

    1.List Vector:线程安全的. ArrayList:适合查找与顺序添加. LinkedList:适合随机插入与删除. 1.1ArrayList与LinkedList的add添加 1.1.1A ...