非旋Treap总结 : 快过Splay 好用过传统Treap
非旋$Treap$
其高级名字叫$Fhq\ Treap$,既然叫$Treap$,它一定满足了$Treap$的性质(虽然可能来看这篇的人一定知道$Treap$,但我还是多说几句:$Fhp\ Treap$就是继承了$Treap$的随机系统,在二叉搜索的基础上,每个点加一个随机化处理,这些随机值满足堆的性质……通俗一点讲,就是$Fhp\ Treap$它每个点有至少两个值,一个是val,即存的数值,这些数值满足二叉搜索树,也就是父亲比左孩子小/大,则右孩子比父亲小/大;还有一个是key,是个随机值,这些随机值满足堆的性质,即父亲比两个孩子都大/小),好了$Fhp\ Treap$和$Treap$的关系到此结束。但是,$Fhp\ Treap$还和左偏树、笛卡尔树,用到了笛卡尔树的建树、左偏树的合并,这使得它不用像$Treap$一样旋转即可做到上述树的性质。
以上都是废话,下面进入正题 PS:本文以大根堆来维持$key$值(第一遍)
$Fhq\ Treap$要维护的值
既然叫$Treap$,就有$Treap$要有的随机值$Key$,还有和其他平衡树一样,要左孩子$l$,右孩子$r$,节点的值$val$和子树大小$siz$、子树节点权值和$sum$。必要的话,还有翻转标记和区间标记。对于作者本人为什么不记录它的$father$,我觉得好像$father$在几个操作中用处不大,好多题似乎没怎么用到$father$这一变量(可以看下面的代码,其实没有$father$就可以做很多操作),不过有$father$确实是一个好习惯。(如有大神指出不对,或者知道哪些$Fhp\ Treap$要用的$father$的地方,一定要跟我讲啊!!!)
当然,我们还要记录当前整棵树的根$rt$和节点编号$cnt$
- int rt,cnt;
- struct Tree
- {
- int l,r,siz,sum,val,key;
- int lazychange;
- bool flagchange,flagreverse;
- }t[MAXN];
两个重要操作
可以说,在非旋$Treap$中,最核心的只有两个:Merge(合并)和Split(分裂),$Fhq Treap$就是用这两个操作打遍天下。
$Split$ 分裂操作
非旋$Treap$有两种分裂操作
一种是按$val$值进行$Split$ (一般用于找该$val$值在树中的位置、$Rank$值等):即分裂后$A$中的叶子节点都比给定的$Val$小(小于等于),$B$中则均大于等于(大于)$Val$。
还有一种则是按个数$Split$(一般用于找第$K$大、区间权值等):即分裂后$A$子树的大小为给定的$Size$,而$B$则为$A$的补集。
其实两种差别不大,我们先看第一种:
(1)按$Val\ Split$
一般$Split$有四个参数,$Now$表示我们现在在搜的子树的根,$W$为给定的标准$val$,还有就是$u,\ v$ 表示$Split$以$Now$为根的子树后得到的两棵树。($u$符合条件,$v$不符合)
1、如果这棵树为空集,则 $Split$后两棵树也为空,$u\ =\ v\ =\ 0$
2、因为以$W$为界,我们只要把$Now$的$val$和$W$比较一下即可,若$Val_Now <= W$ (有些情况为$Val_Now < W$ ),则$Now$满足标准。因为为二叉搜索树,所以$Now$的左孩子$Val$都小于自己,也必满足条件,所以我们可以直接把$Now$和它的左孩子$Split$出来变成一个新的树$u$,此时,我们只要继续做$Now$的右孩子,而前面$Split$出的$u$的右孩子就是$Now$的右孩子$Split$出的满足条件的树,不符合就为$v$的孩子;若$Val_Now >= W$ (有些情况为$Val_Now > W$ ),则$Now$不满足条件,同理$Now$的右孩子也不满足条件,所以我们可以直接把$Now$和它的右孩子$Split$出来变成一个新的树$v$,此时,我们只要继续做$Now$的左孩子,而$Now$新的左孩子就是$Now$的左孩子$Split$出的不满足条件的树$v$,满足则为$u$树的孩子。
大概过程就是这样,不过因为本人语文不太好,听懂的人应该不多……看代码吧……
- inline void Split(int Now,int W,int &u,int &v) // now现在搜的根 以w为界分为以u为根的树和v为根的树(本程序为u中所有节点的val小于等于w)
- {
- if(!Now) u = v = ;
- else
- {
- pushdown(Now);//如果必要时要先下传标记
- if(t[Now].val <= W) u = Now,Split(t[Now].r,W,t[Now].r,v);
- else v = Now,Split(t[Now].l,W,u,t[Now].l);
- update(Now);//最后更新该根的信息
- }
- }
用$&$符号来方便直接将后面的接到前面的左或右孩子上;
(2)按个数来$Split$
和上面的方法一样,只是我们把$W$改成了$Size$
1、和上面一样如果这棵树为空集,则 $Split$后两棵树也为空,$u\ =\ v\ =\ 0$
2、以$Siz$为界,则只要比较$Now$左孩子的$siz_NowLeft$和$Siz$即可,若$siz_L\ >=\ Siz$,则$Now$必不符合条件,不然要有其左孩子,$Size$必要大于标准,所以只要搜$Now$的左孩子,$Split\ Now$和它的右孩子变为$v$即可,其他同上;反之若$siz_L\ <\ Siz$, 则加上$Now$必有$siz_L\ +\ 1\ <=\ Siz$,所以$Split\ Now$和它的左孩子变为$u$即可
- inline void Split(int now,int siz,int &u,int &v)//now现在搜的根 以siz为界 Split后的树的根为u,v(即子树u的大小为siz)
- {
- if(!now) u = v = ;
- else
- {
- pushdown(now);
- if(t[t[now].l].siz >= siz) v = now,Split(t[now].l,siz,u,t[now].l);
- else u = now,Split(t[now].r,siz-t[t[now].l].siz-,t[now].r,v);
- update(now);
- }
- }
$Merge$ 合并操作
一般$Fhq Treap$的操作都是先$Split$再$Merge$之后的,$Split$使其$val$值以满足二叉搜索树(即树$A$所有的$val$值都比$B$中的小),所以只要处理它们的$Key$值,即堆的性质就好了
所以$Fhq Treap$的合并和左偏树的合并非常相似,唯一不同的就是不能$swap$左右孩子,$swap$了那你前面的$Split$使$val$满足二叉搜索树的就全白忙了……
合并的时候,我们传进两棵树的根$u$和$v$
1、有一棵树为空 :直接返回另一棵树
2、合并两棵树:
因为$val$值已在$Merge$中满足的前一棵树的$val$值都比后一棵树的小,所以我们只要按照$Key$值合并。若$u$的$Key$值比$v$的大,因为本文以大根堆来维持$key$值(第二遍),所以新的树一定以$u$为根,定下了根,根据二叉搜索树的性质左边比根小右边比根大,而以$v$为根的子树$val$均大于以$u$为根的树,所以以$v$为根的子树只能接到新的根$u$的右孩子,即$v$和$R_u$进行$Merge$变为新的$R_u$,而左孩子就是原来$u$的左孩子;反之若$u$的$Key$值比$v$的小,因为本文以大根堆来维持$key$值(第三遍),所以新的树一定以$v$为根,同理根据二叉搜索树的性质左边比根小右边比根大,而以$u$为根的子树$val$均小于以$v$为根的树,所以以$v$为根的子树必接到新根$v$的左孩子,即$u$和$L_v$进行$Merge$变为新的$L_v$,右孩子就是原来$v$的右孩子。
最后,更新一下新的树根$u$或$v$并返回新得到的树的根节点即可。
(怕是还是没太懂)看代码吧……
- inline int Merge(int u,int v)
- {
- if(!u||!v) return u + v;
- pushdown(u),pushdown(v);
- if(t[u].key < t[v].key)
- {
- t[v].l = Merge(u,t[v].l);
- return update(v),v;
- }
- else
- {
- t[u].r = Merge(t[u].r,v);
- return update(u),u;
- }
- }
以上就是$Fhq\ Treap$的两个最重要的操作,接下来来看几个必要的操作:
$Build$建树
$Fhq\ Treap$建树和笛卡尔树差不多。
为了保证$Fhq\ Treap$的中序遍历是原数列(或为$val$值递增$/$减),我们可以把它变成一条链!!!没错吧,建树完成了,但显然不优秀,所以就有了$Key$值的随机化。
没有学过笛卡尔树的看这里
首先我们用栈来存我们要做的点, 我们按顺序弹入一个点,因为保证了在它前面的点(即在栈中的点)中序遍历一定在它前面,所以它只能认左孩子或父亲,而且只能根据$Key$值来认。
本文以大根堆来维持$key$值(第四遍)我们先取出栈顶的点,如果栈顶的$Key$值大于该插入的点的值,那么我们直接把该点接在栈顶的右孩子处,当然此时它可能已经有一个右孩子了,很明显原来的右孩子中序遍历也该在应插入点的前面,那我们就把栈顶的右孩子变为它的左孩子,在将其接在栈顶的右孩子处;反之如果栈顶的$Key$值小于等于该插入的点的值 那么它就应该变为该点的左孩子,我们只要弹出栈顶的点,然后继续找到第一个比该插入的点$Key$值大的点即可;当然,万一它是最大的,那我们只要把原栈底的点接在它的左孩子处即可。
这样一来,栈底的点一定是$Key$值最大的点了,也就是这颗树的根啦!
(相信大家还是没看懂,其实你可以手动模拟一遍,在加上以下的代码,相信一定可以懂)
- inline void build()
- {
- int stc[MAXN],sl = ;//用栈来存当前未做的节点
- for(int i = ;i<=n;i++)
- {
- int at = MakeNew(a[i]);//建一个新点
- while(sl&&t[stc[sl]].key < t[at].key) update(stc[sl]),sl--;//比该点小的弹出作为该点的孩子
- if(sl) t[at].l = t[stc[sl]].r,t[stc[sl]].r = at;//更改它父亲和右孩子
- else t[at].l = stc[];//原栈顶接在该节点的左边
- stc[++sl] = at;//进栈
- }
- while(sl) update(stc[sl]),sl--;//未出栈的出栈并update
- rt = stc[];//设置根节点
- }
$Insert$ 插入
先说插入一个点,首先,我们前面说过,有按权值的还有按大小的:
(1)按权值插入:
我们只要找到比它小的数,插到它们的后面。$Split$出比它小(小于等于也行)的即可。
- inline void Insert(int u)
- {
- int x,y;
- Split(rt,u,x,y);
- rt = Merge(Merge(x,MakeNew(u)),y);
- }
(2)按大小插入
其实和上面的一样,只是把$Split$改一下即可
当然呢,插入一堆点嘞,就把那些点建成一棵树,然后当成一个点来插就好啦!
$Makedown$建一个新点
突然忘了怎么建点,但我觉得问题不大
- inline int MakeNew(int val)
- {
- t[++cnt].val = val,t[cnt].key = rand() * rand(),t[cnt].siz = ;
- return cnt;
- }
$Kth$找到第$K$大
- inline int Kth(int now,int sum)
- {
- while()
- if(sum <= t[t[now].l].siz) now = t[now].l;
- else if(sum == t[t[now].l].siz + ) return now;
- else sum-=t[t[now].l].siz + ,now = t[now].r;
- }
以下是P3369 【模板】普通平衡树的代码
- #include<bits/stdc++.h>
- using namespace std;
- inline int read()
- {
- int s = ,w = ;char ch = getchar();
- while(ch<''||ch>''){if(ch == '-')w=-;ch = getchar();}
- while(ch>=''&&ch<=''){s = (s << ) + (s << ) + ch - '';ch = getchar();}
- return s * w;
- }
- int rt,cnt;
- struct Tree
- {
- int l,r,val,key,siz;
- }t[];
- inline int MakeNew(int val)
- {
- t[++cnt].val = val,t[cnt].key = rand() * rand(),t[cnt].siz = ;
- return cnt;
- }
- inline void Split(int now,int w,int &u,int &v)
- {
- if(!now) u = v = ;
- else
- {
- if(t[now].val <= w) u = now,Split(t[now].r,w,t[now].r,v);
- else v = now,Split(t[now].l,w,u,t[now].l);
- t[now].siz = t[t[now].r].siz + t[t[now].l].siz + ;
- }
- }
- inline int Merge(int u,int v)
- {
- if(!u||!v) return u + v;
- if(t[u].key < t[v].key)
- {
- t[u].r = Merge(t[u].r,v);
- t[u].siz = t[t[u].r].siz + t[t[u].l].siz + ;
- return u;
- }
- else
- {
- t[v].l = Merge(u,t[v].l);
- t[v].siz = t[t[v].r].siz + t[t[v].l].siz + ;
- return v;
- }
- }
- inline int Kth(int now,int sum)
- {
- while()
- if(sum <= t[t[now].l].siz) now = t[now].l;
- else if(sum == t[t[now].l].siz + ) return now;
- else sum-=t[t[now].l].siz + ,now = t[now].r;
- }
- int main()
- {
- int n = read(),x,y,z,opt,a;
- srand();
- while(n--)
- {
- opt = read();a = read();
- if(opt == )
- {
- Split(rt,a,x,y);
- rt = Merge(Merge(x,MakeNew(a)),y);
- }
- else if(opt == )
- {
- Split(rt,a,x,z);
- Split(x,a-,x,y);
- y = Merge(t[y].l,t[y].r);
- rt = Merge(Merge(x,y),z);
- }
- else if(opt == )
- {
- Split(rt,a-,x,y);
- printf("%d\n",t[x].siz + );
- rt = Merge(x,y);
- }
- else if(opt == ) printf("%d\n",t[Kth(rt,a)].val);
- else if(opt == )
- {
- Split(rt,a-,x,y);
- printf("%d\n",t[Kth(x,t[x].siz)].val);
- rt = Merge(x,y);
- }
- else if(opt == )
- {
- Split(rt,a,x,y);
- printf("%d\n",t[Kth(y,)].val);
- rt = Merge(x,y);
- }
- }
- return ;
- }
有锅会继续补……
非旋Treap总结 : 快过Splay 好用过传统Treap的更多相关文章
- [模板] 平衡树: Splay, 非旋Treap, 替罪羊树
简介 二叉搜索树, 可以维护一个集合/序列, 同时维护节点的 \(size\), 因此可以支持 insert(v), delete(v), kth(p,k), rank(v)等操作. 另外, prev ...
- 平衡树简单教程及模板(splay, 替罪羊树, 非旋treap)
原文链接https://www.cnblogs.com/zhouzhendong/p/Balanced-Binary-Tree.html 注意是简单教程,不是入门教程. splay 1. 旋转: 假设 ...
- 非旋Treap及其可持久化
平衡树这种东西,我只会splay.splay比较好理解,并且好打,操作方便. 我以前学过SBT,但并不是很理解,所以就忘了怎么打了. 许多用平衡树的问题其实可以用线段树来解决,我们真正打平衡树的时候一 ...
- 非旋 treap 结构体数组版(无指针)详解,有图有真相
非旋 $treap$ (FHQ treap)的简单入门 前置技能 建议在掌握普通 treap 以及 左偏堆(也就是可并堆)食用本blog 原理 以随机数维护平衡,使树高期望为logn级别, FHQ ...
- 2018.08.06 bzoj1500: [NOI2005]维修数列(非旋treap)
传送门 平衡树好题. 我仍然是用的fhqtreap,感觉速度还行. 维护也比线段树splay什么的写起来简单. %%%非旋treap大法好. 代码: #include<bits/stdc++.h ...
- 2018.08.05 bzoj3223: Tyvj 1729 文艺平衡树(非旋treap)
传送门 经典的平衡树问题,之前已经用splay写过一次了,今天我突发奇想,写了一发非旋treap的版本,发现挺好写的(虽然跑不过splay). 代码: #include<bits/stdc++. ...
- 非旋treap套线段树
BZOJ3065. 去年用pascal 块链过了.. 今年来试了试非旋treap大法 注定被块链完爆 代码留这. 第一份 :辣鸡的 垃圾回收做法 跑得极慢 #include <bits/ ...
- 非旋(fhq)Treap小记
前置知识:二叉搜索树 以下摘自 ↑: 二叉搜索树每次操作访问O(深度)个节点. 在刻意构造的数据中,树的形态会被卡成一条链,于是复杂度爆炸 它的复杂度与直接暴力删除类似. 但二叉搜索树扩展性强.更复杂 ...
- 关于非旋FHQ Treap的复杂度证明
非旋FHQ Treap复杂度证明(类比快排) a,b都是sort之后的排列(从小到大) 由一个排列a构造一颗BST,由于我们只确定了中序遍历=a,但这显然是不能确定一棵树的形态的. 由一个排列b构造一 ...
随机推荐
- linux 命令——43 killall(转)
Linux 系统中的killall命令用于杀死指定名字的进程(kill processes by name).我们可以使用kill命令杀死指定进程PID的进 程,如果要找到我们需要杀死的进程,我们还需 ...
- NYOJ-198-数数
原题地址 数数 时间限制:3000 ms | 内存限制:65535 KB 难度:2 描述 我们平时数数都是喜欢从左向右数的,但是我们的小白同学最近听说德国人数数和我们有些不同,他们正好和我们相 ...
- IOS 拖拽事件(手势识别)
@interface NJViewController () @property (weak, nonatomic) IBOutlet UIView *customView; @end @implem ...
- Android(java)学习笔记103:Framework运行环境之 Android进程产生过程
1. 前面Android(java)学习笔记159提到Dalvik虚拟机启动初始化过程,就下来就是启动zygote进程: zygote进程是所有APK应用进程的父进程:每当执行一个Android应用程 ...
- C#的接口基础教程之三 定义接口成员
接口可以包含一个和多个成员,这些成员可以是方法.属性.索引指示器和事件,但不能是常量.域.操作符.构造函数或析构函数,而且不能包含任何静态成员.接口定义创建新的定义空间,并且接口定义直 接包含的接口成 ...
- 操作系统(3)_CPU调度_李善平ppt
不只上面的四种,比如时间片到了也会引起调度. 具体的调度算法: fcfs简单,但是波动很大. 最高相应比算法,执行时间最长就应该等待的长点,比sjf多了一个等待时间的考虑. 硬件定时器和软件计数器共同 ...
- 问题008:java 中代码块的风格有几种?单行注释可否嵌套?多行注释可否嵌套?
有两种:一种是次行风格,英文称为next-line 一种是是行尾风格,英文称为 end-of-line 举例 行尾风格 public class HelloWorld{ public static v ...
- c++ question 003 求两数大者?
#include <iostream>using namespace std; int main(){ //求两数中的大者? int a,b; cin>>a>>b; ...
- Java泛型和反射
1. 字节码对象的三种获取方式 以String为例 Class<? extends String> strCls = "".getClass(); Class<S ...
- tcl之关于TCL