非旋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 = 0;
    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 = 0;
    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-1,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 = 0;//用栈来存当前未做的节点
    for(int i = 1;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[1];//原栈顶接在该节点的左边
        stc[++sl] = at;//进栈
    }
    while(sl) update(stc[sl]),sl--;//未出栈的出栈并update
    rt = stc[1];//设置根节点
}

$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 = 1;
    return cnt;
}

$Kth$找到第$K$大

inline int Kth(int now,int sum)
{
    while(1)
        if(sum <= t[t[now].l].siz) now = t[now].l;
        else if(sum == t[t[now].l].siz + 1) return now;
        else sum-=t[t[now].l].siz + 1,now = t[now].r;
}


以下是P3369 【模板】普通平衡树的代码

#include<bits/stdc++.h>
using namespace std;
inline int read()
{
    int s = 0,w = 1;char ch = getchar();
    while(ch<‘0‘||ch>‘9‘){if(ch == ‘-‘)w=-1;ch = getchar();}
    while(ch>=‘0‘&&ch<=‘9‘){s = (s << 1) + (s << 3) + ch - ‘0‘;ch = getchar();}
    return s * w;
}
int rt,cnt;
struct Tree
{
    int l,r,val,key,siz;
}t[110000];
inline int MakeNew(int val)
{
    t[++cnt].val = val,t[cnt].key = rand() * rand(),t[cnt].siz = 1;
    return cnt;
}
inline void Split(int now,int w,int &u,int &v) // now???????? ??val???????u?????????v???????
{
    if(!now) u = v = 0;
    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 + 1;
    }
}
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 + 1;
        return u;
    }
    else
    {
        t[v].l = Merge(u,t[v].l);
        t[v].siz = t[t[v].r].siz + t[t[v].l].siz + 1;
        return v;
    }
}
inline int Kth(int now,int sum)
{
    while(1)
        if(sum <= t[t[now].l].siz) now = t[now].l;
        else if(sum == t[t[now].l].siz + 1) return now;
        else sum-=t[t[now].l].siz + 1,now = t[now].r;
}
int main()
{
    int n = read(),x,y,z,opt,a;
    srand(20040328);
    while(n--)
    {
        opt = read();a = read();
        if(opt == 1)
        {
            Split(rt,a,x,y);
            rt = Merge(Merge(x,MakeNew(a)),y);
        }
        else if(opt == 2)
        {
            Split(rt,a,x,z);
            Split(x,a-1,x,y);
            y = Merge(t[y].l,t[y].r);
            rt = Merge(Merge(x,y),z);
        }
        else if(opt == 3)
        {
            Split(rt,a-1,x,y);
            printf("%d\n",t[x].siz + 1);
            rt = Merge(x,y);
        }
        else if(opt == 4) printf("%d\n",t[Kth(rt,a)].val);
        else if(opt == 5)
        {
            Split(rt,a-1,x,y);
            printf("%d\n",t[Kth(x,t[x].siz)].val);
            rt = Merge(x,y);
        }
        else if(opt == 6)
        {
            Split(rt,a,x,y);
            printf("%d\n",t[Kth(y,1)].val);
            rt = Merge(x,y);
        }
    }
    return 0;
}

有锅会继续补……



非旋Treap总结 : 快过Splay 好用过传统Treap

原文地址:https://www.cnblogs.com/rockyyh/p/10079618.html

时间: 2024-08-02 02:55:55

非旋Treap总结 : 快过Splay 好用过传统Treap的相关文章

非旋 treap 结构体数组版(无指针)详解,有图有真相

非旋  $treap$ (FHQ treap)的简单入门 前置技能 建议在掌握普通 treap 以及 左偏堆(也就是可并堆)食用本blog 原理 以随机数维护平衡,使树高期望为logn级别, FHQ 不依靠旋转,只有两个核心操作merge(合并)和split(拆分) 所谓随机数维护平衡就是给每个节点一个随机值 key (下文中没有加随机的就代表是真实权值), 然后整棵树中 key 值要满足小(大)根堆的性质(也就是heap), 同时也要满足平衡树(tree)的性质(也就是每个节点左子树内节点真实

沉迷数据结构1(treap&amp;非旋treap)

Achen大佬说不要沉迷数据结构否则智商会降低的. 从省选考完后就开始学treap,首先是自己yy了一个打了两百多行,然后debug了2个月还是3个月记不清了. 最后弃疗,去找了网上别人的代码抄了一遍. noip考完后补常规的一段时间,羡慕Achen能20分钟打出一个treap模板,于是自己也开始走上打板子的不归路. 到了后来可以10分钟左右打出一个结构体版的treap,看了Achen的数组版treap,觉得自己结构体版的太不优秀啦,于是就换成数组版的. 然后现在有几周没有碰过treap,感觉又

2827: 千山鸟飞绝 非旋treap

国际惯例的题面:看起来很不可做的样子,我们先来整理一下题意吧.就是,维护每个点曾经拥有过的最大的两个属性值,支持把点的位置移动.我们用map对每个位置进行离散化,对每个位置建立一个平衡树.为了方便分离,这个平衡树的关键字是节点编号.然后把每个点当做一个节点,放进其所在位置的平衡树里.剩下要做的就是平衡树分离出一个点,合并一个点,还有打标记了.对于士气值的标记,我们维护平衡树中的max,每次合并的时候,用这个新点的威武值去给整棵树打标记,再用树中的max给这个新点打标记.团结值的标记,合并后一起打

非旋Treap

最近看到有一种不用旋转的treap,好像还可以持久化,于是就学了一下. 如果你还不会Treap的话,那你可以点击这里,对旋转Treap有个大致了解,这里就不赘述Treap的性质了. treap就是tree+heap.它的每个节点的权值data满足排序二叉树的性质,随机权值key满足堆的性质.由于key是随机的所以它大致是平衡的. 不基于旋转的treap有两个基本操作: merge(a,b):返回一个treap,包含a,b两个treap中的所有节点,但要保证b中所有节点权值都大于等于a. spli

[BZOJ 3224] 普通平衡树 非旋Treap

题意 维护一个多重集合 $S$ , 支持: ① 插入一个数 $w$ . ② 删除一个数 $w$ . ③ 查询 $w$ 在集合中的排名. ④ 查询集合中排名第 $r$ 的数. ⑤ 求集合中 $w$ 的前驱. ⑥ 求集合中 $w$ 的后继. $N \le 100000$ . 小结 要总结一些常见的写法和想法, 减小实现时的复杂度. 实现 #include <cstdio> #include <cstring> #include <cstdlib> #include <

fhq treap ------ luogu P3369 【模板】普通平衡树(Treap/SBT)

二次联通门 : LibreOJ #104. 普通平衡树 #include <cstdio> #include <iostream> #include <algorithm> const int BUF = 12312323; char Buf[BUF], *buf = Buf; inline void read (int &now) { bool temp = false; for (now = 0; !isdigit (*buf); ++ buf) if (*

【基本算置顶】各大算法&amp;&amp;数据结构模板

板子,全是板子 更新日志(从2018.11.19开始) 2018.12.02 : 更新了数据结构->扫描线 2018.11.22 : 更新了数据结构->平衡树->FHQ Treap->维护区间操作 2018.11.20 : 更新了数论->博弈论->nim游戏 2018.11.20 : 更新了数据结构->平衡树->FHQ Treap 观摩本蒟蒻板子库的大佬数: 不断更新 一.数论 1.快速幂 2.欧拉函数 3.乘法逆元(线性求逆) 4.线性筛素数 5.扩展欧几

[Bzoj3223][Tyvj1729] 文艺平衡树(splay/无旋Treap)

题目链接:https://www.lydsy.com/JudgeOnline/problem.php?id=3223 平衡树处理区间问题的入门题目,普通平衡树那道题在维护平衡树上是以每个数的值作为维护的标准,而处理区间问题时,维护平衡树的应该是每个位置的下标,所以平衡树中序遍历时应该是当前区间的样子.例如: {1 2 3 4 5}翻转区间1 3,则中序遍历应该输出{3,2,1,4,5}. 提供splay和无旋Treap的做法. splay做法: 1 #include<bits/stdc++.h>

非旋转Treap

Treap是一种平衡二叉树,同时也是一个堆.它既具有二叉查找树的性质,也具有堆的性质.在对数据的查找.插入.删除.求第k大等操作上具有期望O(log2n)的复杂度.     Treap可以通过节点的旋转来实现其维持平衡的操作,详见旋转式Treap. 而旋转式Treap在对区间数据的操作上无能为力,这就需要非旋转式Treap来解决这些区间问题. 非旋转式Treap支持的操作 基本操作: 操作 说明 实现复杂度 Build 构造Treap O(n) Merge 合并Treap O(log2n) Sp