平衡树·从BST到treap到fhq-treap

Treap详解 & Treap代码注意事项与总结 文章目录BSTDescriptionSolutionTreapTipsCode 首先,我们学习一个比较简单的东西: BST(Binary Search Tree),即二叉排序树。 BST Description Solution 二叉排序树是满足下面条件的一棵二叉树: 对于任何一个节点rtrtrt,它的关键码大于所有在其左子树内的关键码,但是小于所有在其右子树内的关键码。这也被称为BST性质。 对于某些题目,我们需要在修改的同时维护这个数据结构,在查询的时候快速得到答案。我们先假设任何时候二叉排序树内不存 阅读详情

1.BST

1.1 BST是啥

要说平衡树必须要从BST( B i n a r y   S e a r c h   T r e e Binary\ Search\ Tree Binary Search Tree)说起

我们定义一棵BST满足一下性质

1.是一棵二叉树,既每个点最多拥有两个儿子
2.对于每个节点,满足他的左儿子 < < <他 < < <他的右儿子

是不是很简单呢?

1.2 BST的基本操作

我们发现,利用BST的这个特殊的性质,可以解决很多东西
虽然大家肯定也不会写BST,但是这一部分建议大家还是读一读,因为后面的一些过程和这里是差不多的

1.2.1 插入x

我们发现,对于给定的一棵BST,我们是可以定位出下一个东西是在哪里的
具体流程是:
1.从根开始查找
2.对于经过的每一个点 i i i,如果 x < v a l i x<val_i x<vali​,根据定义,我们应该查询他的左子树,反之则右子树,当 i = 0 i=0 i=0的时候就是我们要插入的地方

当然如果 x = v a l i x=val_i x=vali​的时候可能还需要出现特判的情况,这就需要在题目中具体情况具体分析。当然你以后肯定也不会写这玩意

1.2.2 删除x

同样的,我们按照插入的时候的方法,找到我们要删除的位置,然后直接断掉一条边就可以

1.2.3 查询x的排名

这个东西怎么做呢?
我们还是从根节点开始查找,记录一个 r e s res res表示答案
对于我们查到到的一个点 i i i
如果 x < v a l i x<val_i x<vali​, i = l c i=lc i=lc
如果 x = v a l i x=val_i x=vali​,那么我们就要查的是 i i i的排名,答案应该是 r e s res res加上左子树的大小再加上一对吧,也就是 r e s = r e s + s i z [ l c ] + 1 res=res+siz[lc]+1 res=res+siz[lc]+1
如果 x > v a l i x>val_i x>vali​,说明 i i i在右子树,那么所有的左子树的部分和 i i i都比 x x x小,所以 r e s = r e s + s i z [ l c ] + 1 , u = r c res=res+siz[lc]+1,u=rc res=res+siz[lc]+1,u=rc

很好理解,下面给出简要代码,为了后面写的方便,我们用 s o n [ u ] [ 0 ] son[u][0] son[u][0]表示 u u u的左子树, 1 1 1表示右子树

int rnk(int x){
    int u=rt,res=0;
    while(1){
        if(val[u]>x)u=lc;
        else if(val[u]==x)return res+siz[son[u][0]]+1;
        else res+=siz[son[u][0]]+1,u=son[u][1];
    }
}

1.2.4 查询排名为x的数

这个东西根上面就是非常类似的了
就是把上面的过程反过来了
如果 x ≤ s i z [ l c ] x\leq siz[lc] x≤siz[lc],则 i = l c i=lc i=lc
否则如果 x ≤ s i z [ l c ] + 1 x\leq siz[lc]+1 x≤siz[lc]+1,则返回 v a l i val_i vali​
否则 x = x − s i z [ l c ] − 1 , i = r c x=x-siz[lc]-1,i=rc x=x−siz[lc]−1,i=rc

代码:

int kth(int x){
    int u=rt;
    while(1){
        if(siz[son[u][0]]>=x)u=son[u][0];
        else if(siz[son[u][0]]+1>=x)return val[u];
        else x-=siz[son[u][0]]+1,u=son[u][1];
    }
}

1.2.5 查询x在数中的编号
这个不难,一直往下走就可以,根据权值决定走做左子树还是右子树

int find(int x){
    int u=rt;
    while(u&&son[u][x>val[u]]!=x)u=son[u][x>val[u]];
    return u;
}

这个地方我们就看出来为什么要写 s o n [ u ] [ 0 ] , s o n [ u ] [ 1 ] son[u][0],son[u][1] son[u][0],son[u][1]了

1.2.6 查询x的前驱
这里 x x x的前驱定义为小于 x x x的最大的数

这个其实很简单,首先要找到 x x x,然后考虑BST的定义就可以,首先他的前驱一定在他的左子树里面。然后我们要找最大,所以不停的往右子树走,直到没有右子树,他就是最大的

代码:

int pre(int x){
    int u=find(x);
    u=son[u][0];
    while(son[u][1])u=son[u][1];
    return u;
}

1.2.7 查询x的后继
和上一个非常像,不过是先往右子树走再往左子树走

1.3 时间复杂度

这个东西因为是一个二叉树,那么每次查询的复杂度最坏的时候就是树的深度,但是因为每个节点的插入顺序不同会导致不同形态的BST,那么很容易被退化成一条链,那么这个时候查询一次的复杂度就是 Θ ( n ) \Theta(n) Θ(n),就是平方级别的了

下面给出几种退化的情况(其实很好卡对吧)

在这里插入图片描述

那么 Θ ( n 2 ) \Theta(n^2) Θ(n2)的效率我们显然是无法接受的(和暴力都一样了要他有啥用),所以我们考虑如何优化——于是,平衡树诞生了!

2.从BST到平衡树

我们发现,我们让复杂度尽量的低的一种处理方法就是,让左右两颗子树大小差尽量的小,那么深度就会尽量小。我们把通过一些奇奇怪怪的处理方法使得期望深度为 log ⁡ n \log n logn级别的BST的优化叫做平衡树

平衡树的实现方法有很多,最常见的有

s p l a y splay splay系,通过旋转使得每次操作均摊 Θ ( log ⁡ n ) \Theta(\log n) Θ(logn) 优点是代码短,理解难度低,可以套LCT,是万能的一种平衡树,但是缺点是常数太大,被各种吊打

t r e a p treap treap系,通过旋转+玄学使得每次操作均摊 Θ ( log ⁡ n ) \Theta(\log n) Θ(logn)(我们接下来会讲到) 优点是 f h q   t r e a p fhq\ treap fhq treap是代码最短的平衡树了吧,同时能够基本实现 s p l a y splay splay的各项操作并且快。但是原 t r e a p treap treap的局限性很大,不能维护序列上的问题。包括 f h q   t r e a p fhq\ treap fhq treap在内的 t r e a p treap treap系平衡树都有一个问题就是有点看脸(虽然一般不会被卡),另外 f h q   t r e a p fhq\ treap fhq treap维护LCT会比 s p l a y splay splay维护多一个 log ⁡ \log log

其他快速平衡树,比如SGT,SBT,AVL,RBT等等,都是利用一些奇奇怪怪的技巧让他尽量平衡。优点是常数小,速度快,但是功能都没有 s p l a y splay splay齐全,而且码量都很长(比如SBT,RBT板子码量都在200+左右,但是 s p l a y , t r e a p splay,treap splay,treap写的好看一点都只用80左右)

那么下面我们会着重讲解 t r e a p treap treap系的平衡树,如果想学 s p l a y splay splay也可以点这里

3.treap

3.1 treap是啥

这个词啥意思啊?你翻遍字典也找不着(学术界的或许有),因为这个词本来就是人造出来的,他从这两个词合成的

t r e a p = t r e e + h e a p treap=tree+heap treap=tree+heap

所以一般翻译出来就叫树堆,当然一般没人读中文叫法

t r e e tree tree好理解,二叉查找树嘛,那 h e a p heap heap是怎么来的呢?这就是 t r e a p treap treap的精髓所在

t r e a p treap treap的核心思想是

  • 让 v a l val val满足二叉查找树性质,既 t r e e tree tree
  • 在新储存一个键值,使得这整颗 t r e a p treap treap的键值满足堆性质(我习惯小根堆)

那么键值怎么来呢?题目里又没给。 方法很简单,没有就随机一个嘛,所以这个键值我们就把它命名成 r n d rnd rnd好了(在实际写程序中我习惯写成 t r e a p treap treap)

可能光说不太好理解,配张图

在这里插入图片描述

比如在棵 t r e a p treap treap中, v a l val val满足全部BST性质,并且 r n d rnd rnd满足堆性质,既每个点 r n d ≤ rnd\leq rnd≤他的两个子树的 r n d rnd rnd

然后我们再看几个基本操作

3.2 treap的基本操作

3.2.1 插入x

首先,我们先创建处新的节点,也就是将 r n d rnd rnd随机出来,这里以插入为例

在这里插入图片描述

然后按照权值找到这个点应该加在哪里(也就是BST的插入操作)

在这里插入图片描述

那么这个时候我们的权值是满足条件了,但是 r n d rnd rnd又不满足堆性质了啊
所以我们需要进行上调
等会别着急,这个上调不能直接把它和他爸爸交换啊,要不然 v a l val val的性质又不对了啊

这怎么办啊…
学过 s p l a y splay splay的同学们一定知道,我们需要通过左旋右旋的方式来进行调整

在这里插入图片描述

这里就是左右旋,左旋之后再右旋又回去了,这两个是相对的

大家观察之后会发现,这样旋转之后,新的树仍然满足性质,这个思想也就是 s p l a y splay splay的核心思想

然后我们进行分部上调

回到刚才的过程中,这里我们需要左旋

在这里插入图片描述

变成

在这里插入图片描述

继续右旋

在这里插入图片描述

然后旋转完了之后满足条件还需要把相应的信息更新了,比如说 s i z siz siz什么的

那么这个时候我们就成功的在 t r e a p treap treap中插入的一个数(真累啊)

3.2.2 删除x
一个比较常见的操作是找到该点,然后把那个点的值调为 + ∞ +\infty +∞,然后再维护堆性质,这个时候这个点就会下去了,具体看演示

在这里插入图片描述

我们要删除27,先找到该节点,然后将随机值改成inf,然后比较左右子树的 r n d rnd rnd大小,把根转到叶子,这个时候和插入就不太一样了

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

以此类推,等他转到最下面的时候,直接断掉他和他爸爸之间的边就可以,这样我们就完成了删除节点的操作

3.2.3 其他操作

和BST基本一样,详见上面说的1.2.3到1.2.7部分

3.3 代码

相信经过上面的讲解最关键的问题已经都解决了,接下来就是写代码了

但是因为我从来没写过treap所以这里放一个简单写的核心代码,既插入和删除,其他部分上面我们都有讲到,应该不难写

void rotate(int &u, int d){//x代表的是旋转时作为父节点的节点,d代表的是旋转的方向
//d==0时是左儿子旋上来, d==1是右儿子旋上来.
    int c=son[u][d];
    son[u][d]=son[c][d^1];
    son[c][d^1]=u; 
    update(u),update(u=c);//更新相应信息
}
void insert(int &u, int val){
    if(!u){//找到对应位置就新建节点
        u=++tot;//节点数
        cnt[u]=siz[u]=1;
       val[u]=val,rnd[u]=rand();
        return;
    }
    siz[u]++;//因为插入了数,所以在路径上每个节点的size都会加1
    if(val[u]==val){cnt[u]; return;}//找到了直接返回
    int d=val[u]<val;
    insert(son[u][d],val);//否则递归查找插入位置
    if(rnd[u]>rnd[son[u][d]])rotate(u,d);
}
void delet(int &u, int val){
    if(!u) return;//防止越界
    if(val[u]==val){
        if(cnt[u]>1){cnt[u]--,siz[u]--;return;}//有相同的就直接cnt--
        bool d=rnd[son[u][0]]>rnd[son[u][1]];
        if(!son[u][0]||!son[u][1])u=son[u][0]|son[u][1]//只有一个儿子就直接把那个儿子放到这个位置
        else rotate(u,d), delet(u,val);//否则将x旋下去,找一个随机值小的替代,直到回到1,2种情况
    }
    else siz[u]--, delet(son[u][val>val[u]], val);//递归找到要删除的节点.
}

3.4 treap一些问题

3.4.1 时间复杂度
t r e a p treap treap的时间其实算是靠脸拿分,如果你脸比较黑生成的随机数刚好让你的 t r e a p treap treap退化成一条链,那也没辙。

但是这种情况一般不会出现,只要你没有忘掉写srand(time(0)),当然虽然大部分题目不写这一行也能AC,但是很容易会退化,甚至比 s p l a y splay splay还要慢

3.4.2 功能
我们发现这些单点修改之类的问题 t r e a p treap treap都是可以支持的,但是我们发现,我们无法对于一段区间进行求和之类的操作,因此功能比较局限

而且转来转去好麻烦啊…

3.4.3 优化
有没有一种 t r e a p treap treap既可以维护一段区间上的问题,又可以不用旋转呢?
没错,是有的,范浩强大佬(%)发明了这样的一种 t r e a p treap treap,因此这样的 t r e a p treap treap就叫做 f h q   t r e a p fhq\ treap fhq treap

同时根据是否旋转,最本身的 t r e a p treap treap叫做双旋 t r e a p treap treap,而 f h q   t r e a p fhq\ treap fhq treap叫做无旋 t r e a p treap treap

f h q   t r e a p fhq\ treap fhq treap的优点有很多很多,比如说可以算是码量最小的平衡树了,同时比 s p l a y splay splay快,虽然稍微比 s p l a y splay splay难理解,但是结合图形还是不难理解的

那么接下来,我们就来讲解一下 f h q   t r e a p fhq\ treap fhq treap,他的根本思想是通过分裂和合并提取出我们想要的部分

t r e a p treap treap?被 f h q fhq fhq碾压好不好

4.fhq-treap

4.1 分裂

我们要把 f h q   t r e a p fhq\ treap fhq treap分裂成两棵平衡树,但是还需要有个条件吧,我们把所有权值 ≤ k \leq k ≤k的分到左树,剩下的分到右树

为了方便,我们用 x x x表示左树根, y y y表示右树根

这个不太好理解,上图

在这里插入图片描述

我们从根节点向下找,我们发现 22 22 22是比 27 27 27小的,所以 22 22 22这个点应该在左树中,既 x = 22 所 在 节 点 编 号 x=22所在节点编号 x=22所在节点编号,而且根据BST性质,他的左子树也应该在左树里面,所以我们拿右子树进行分裂

在这里插入图片描述

然后我们看右子树, 31 31 31比 27 27 27大,所以我们拿这个点的左子树进行分裂,而 31 31 31和他的右子树就应该属于分裂之后的右树

在这里插入图片描述

分裂左子树, 27 = 27 27=27 27=27,应该划到左树里,相同的

在这里插入图片描述

继续操作,直到分裂完成

在这里插入图片描述

最后就变成了这个样子的两棵树

在这里插入图片描述

但是这个东西看上去不太好写啊?

s p l i t split split其实有两种分裂方法,一种是按照权值,把权值 ≤ k \leq k ≤k的分裂,当然另一种是按照排名分裂,根据不同的题目各有不同

void split_by_val(int o,int &u,int &v,int k)//o表示当前访问的原平衡树节点,u表示如果分裂到左子树要接到哪里(所以要引用),v表示如果分裂到右子树要接到哪里,k表示按多少分裂
{
    if(!o){u=v=0;return;}//边界条件
    if(val[o]<=k)split_by_val(son[u=o][1],son[o][1],v,k);//应当放到左子树,所以u=o,同时需要分裂o的右子树,所以要分裂的就是son[o][1],v不动
    else split_by_val(son[v=o][0],u,son[o][0],k);//同理,需要往右子树分裂
    update(o);//因为他的子树发生了变化,所以要更新o的信息
}

另一个是按照排名分裂

void split_by_rank(int o,int &u,int &v,int k){
    if(!o){u=v=0;return;}//边界条件
    int rank=siz[son[o][0]]+1;//计算排名
    if(rank<=k)split_by_rank(son[u=o][1],son[o][1],v,k-rank);//应该往右子树进行分裂,但是这个时候我们分裂的排名就应该是k-rank了
    else split_by_rank(son[v=o][0],u,son[o][1],k);//往左子树分裂不用懂
    update(o);
}

然后分裂就没了,程序也很短呢>_<

4.2 合并

要分裂还要合并回去啊,怎么合并呢?注意这里合并要满足我们合并的两棵树 u , v , max ⁡ ( v a l u ) ≤ min ⁡ ( v a l v ) u,v,\max(val_u)\leq\min(val_v) u,v,max(valu​)≤min(valv​)才能合并,否则只能启发式合并。

那么怎么合并呢?我们发现因为 r a n k rank rank满足相应的关系,所以我们只需要考虑合并之后的 r n d rnd rnd满足堆性质就可以

比如我们要把这两个东西合并上来,我们比较两个根的 r n k rnk rnk,发现 22 22 22那个点的 r n d rnd rnd比较小,所以我们为了维护小根堆的性质,应该把 22 22 22作为合并之后的新的根。

在这里插入图片描述

然后他的左儿子是不用变的,我们需要用右儿子 25 25 25和 31 31 31进行合并

在这里插入图片描述

然后我们再比较这个时候的两个根 25 , 31 25,31 25,31发现 31 31 31的 r n d rnd rnd比较小,所以把它拎上来

在这里插入图片描述

那么我们就需要拿 31 31 31的右儿子和 25 25 25进行合并

在这里插入图片描述

再经过比较,我们就得到了

在这里插入图片描述

这样我们就合并完了,我觉得合并比分裂更好理解一点吧

下面是程序

int merge(int u,int v){
    if(!u||!v)return u|v;//边界情况,如果有一个待合并子树空了就返回另一个
    int rt;//合并之后的根
    if(treap[u]<treap[v])son[rt=u][1]=merge(son[u][1],v);//合并右子树(treap就是rnd)
    else son[rt=v][0]=merge(u,son[v][0]);//合并左子树
    return update(rt),rt;
}

4.3 其他操作

4.3.1 插入x

这个东西怎么做呢?显然不能按照之前的遍历的方法了,为什么呢?因为 r n d rnd rnd不满足小根堆了,于是,旋转…

那怎么做呢?之前说的 m e r g e merge merge和 s p l i t split split是干啥的?

所以插入一个数 k k k的流程就是

1. s p l i t ( r t , x , y , k ) split(rt,x,y,k) split(rt,x,y,k),注意这里是按照权值 s p l i t , x , y split,x,y split,x,y是我们单独开的两个变量表示分裂之后的两个根

2. r t = m e r g e ( m e r g e ( x , n e w n o d e ( k ) ) , y ) rt=merge(merge(x,newnode(k)),y) rt=merge(merge(x,newnode(k)),y)

也就是说我们只要在这两个子树之间添加一个点就可以了,然后合并回去,大功告成

代码在下面

4.3.2 删除x

这个东西怎么做呢?可以先把 ≤ x − 1 \leq x-1 ≤x−1的拆出来,再把 > x >x >x的拆出来,但是这样真的对吗?有可能有很多个 v a l = x val=x val=x的对吧,但是我们只要删掉一个,怎么办呢?

删除 k k k的流程

1. s p l i t ( r t , x , z , k ) split(rt,x,z,k) split(rt,x,z,k)
2. s p l i t ( x , x , y , k − 1 ) split(x,x,y,k-1) split(x,x,y,k−1)
3. y = m e r g e ( s o n [ y ] [ 0 ] , s o n [ y ] [ 1 ] ) y=merge(son[y][0],son[y][1]) y=merge(son[y][0],son[y][1])这里我们把他的两棵子树合并起来相当于只删掉了一个点了
4. r t = m e r g e ( m e r g e ( x , y ) , z ) rt=merge(merge(x,y),z) rt=merge(merge(x,y),z)注意顺序

4.3.3 查询x的排名

这个怎么办呢?当然我们可以按照BST的方法,但是我们比较懒怎么办呢?

查询 k k k的排名的流程

1. s p l i t ( r t , x , y , k − 1 ) split(rt,x,y,k-1) split(rt,x,y,k−1)这里必须是 k − 1 k-1 k−1
2. p r i n t   s i z x + 1 print\ siz_x+1 print sizx​+1分裂后左树都比他小,再 + 1 +1 +1
3. r t = m e r g e ( x , y ) rt=merge(x,y) rt=merge(x,y)还原

4.3.4 查询排名为x的数
这个东西当然可以仿照上面的写一个按照 r a n k rank rank的 s p l i t split split
但是太麻烦了,我们还要再写一个 s p l i t split split,所以这里我们一般使用BST的查询方式

4.3.5 查询x的前驱

思路就是先裂出来,然后再找左边最大的

查询 k k k的前驱的流程

1. s p l i t ( r t , x , y , k − 1 ) split(rt,x,y,k-1) split(rt,x,y,k−1)
2.查找 x x x中最大的就是前驱
3. r t = m e r g e ( x , y ) rt=merge(x,y) rt=merge(x,y)

4.3.6 查询x的后继

和查询前驱没有什么本质上的区别

4.3.7 查询区间情况

这个也不难,我们可以利用两次 s p l i t split split拎出来需要查询的那一段,让后获得答案之后再合并回去就好了

4.4 代码

这里是洛谷模板题的代码,当然不是加强版

为了清晰一点我把每次操作都写成了函数

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

# define Rep(i,a,b) for(int i=a;i<=b;i++)
# define _Rep(i,a,b) for(int i=a;i>=b;i--)
# define RepG(i,u) for(int i=head[u];~i;i=e[i].next)

typedef long long ll;

const int N=1e5+5;

template<typename T> void read(T &x){
   x=0;int f=1;
   char c=getchar();
   for(;!isdigit(c);c=getchar())if(c=='-')f=-1;
   for(;isdigit(c);c=getchar())x=(x<<1)+(x<<3)+c-'0';
    x*=f;
}

int n;
int tot;
int son[N][2],val[N],siz[N],treap[N];
int rt,x,y,z;

int newnode(int x){
    int u=++tot;
    son[u][0]=son[u][1]=0;
    val[u]=x,siz[u]=1;
    treap[u]=rand();
    return u;
}

void update(int x){
    siz[x]=siz[son[x][0]]+siz[son[x][1]]+1;
}

int merge(int u,int v){
    if(!u||!v)return u|v;
    int rt;
    if(treap[u]<treap[v])son[rt=u][1]=merge(son[u][1],v);
    else son[rt=v][0]=merge(u,son[v][0]);
    return update(rt),rt;
}

void split(int o,int &u,int &v,int k){
    if(!o){u=v=0;return;}
    if(val[o]<=k)split(son[u=o][1],son[o][1],v,k);
    else split(son[v=o][0],u,son[o][0],k);
    update(o);
}

void ins(int k){
    split(rt,x,y,k);
    rt=merge(merge(x,newnode(k)),y);
}

void del(int k){
    split(rt,x,z,k);
    split(x,x,y,k-1);
    y=merge(son[y][0],son[y][1]);
    rt=merge(merge(x,y),z);
}

void rnk(int k){
    split(rt,x,y,k-1);
    printf("%d\n",siz[x]+1);
    rt=merge(x,y);
}

int kth(int k){
    int u=rt;
    while(1){
        if(siz[son[u][0]]>=k)u=son[u][0];
        else if(siz[son[u][0]]+1>=k)return val[u];
        else k-=siz[son[u][0]]+1,u=son[u][1];
    }
}

int pre(int k){
    split(rt,x,y,k-1);
    int u=x;
    while(son[u][1])u=son[u][1];
    rt=merge(x,y);
    return u;
}

int nxt(int k){
    split(rt,x,y,k);
    int u=y;
    while(son[u][0])u=son[u][0];
    rt=merge(x,y);
    return u;
}

int main()
{
    srand(19260817);
    read(n);
    Rep(i,1,n){
        int opt,x;
        read(opt),read(x);
        switch(opt){
            case 1:ins(x);break;
            case 2:del(x);break;
            case 3:rnk(x);break;
            case 4:printf("%d\n",kth(x));break;
            case 5:printf("%d\n",val[pre(x)]);break;
            case 6:printf("%d\n",val[nxt(x)]);break;
        }
    }
    return 0;
}

4.5 fhq-treap的一些问题

4.5.1 时间复杂度

通常来说只要不脸黑应该是比 s p l a y splay splay和普通 t r e a p treap treap快的,因为旋转操作常数太大

当然理论复杂度还是 Θ ( n log ⁡ n ) \Theta(n\log n) Θ(nlogn)

4.5.2 srand

写 t r e a p treap treap千万别忘了写 s r a n d srand srand,虽然能对,但是说不定就被卡没了

关于 s r a n d srand srand里面的数,srand(time(0))是可以的,但是不太好调试,可能会写出一些锅有的时候随机的数是对的,有的时候又错了,所以在调试的时候建议大家写srand(一个比较大的数),比如说 19260817 , 1919810 19260817,1919810 19260817,1919810之类的,让随机种子固定。或者当然手写随机数也是可以的

5.写在最后

这篇博客我大概写了快三个小时?
里面的图都是从我老师那里盗的(bushi

FHQ-Treap(非旋treap/平衡树)——从入门到入坟 平衡树有很多种,其中fhq-treap 可以说是最强的一种平衡树之一,它可以维护值域,也可以维护下标,还能维护区间修改,更难能可贵的是,它可以完成splay都完成不了的可持久化操作。其唯一弱于splay的就是在LCT上,splay维护LCT的复杂度是O(nlog(n)),而fhq-treap的复杂度为O(nlog^2(n)),稍微大了一点,但是比splay好写多了。............ 阅读详情

相关推荐

【python】epub的电子书繁体字与简体字转换

中文繁体与简体转换

xiexianyou666的博客 3088

【高阶数据结构】平衡二叉树(AVL)的插入(4种图解旋转方法)

一篇文章彻底学会平衡二叉树的4种旋转方法,配图讲解超清晰!LL型插入,RR型插入,LR型插入和RL型插入

更新C/C++,数据结构与算法,计算机视觉,深度学习,目标检测 4215

多模态情感识别(MER)数据集实战指南:从IEMOCAP到应用落地

本文提供了多模态情感识别(MER)从数据集到应用落地的完整实战指南。以经典IEMOCAP数据集为例,详细讲解了音频、视觉、文本等多模态数据的预处理、特征提取与融合技术,并分享了模型训练调优心得及面向智能客服、在线教育等场景的部署考量,帮助开发者快速构建高效可用的MER系统。

h3i4j的博客 1069

Treap=Tree+Heap! 各种BST大PK(模板)

刚开学时间确实比较多,于是便继续补寒假的坑。         看了标题应该就很明确了,此篇就是讲又一种平衡树Treap的。可以说Treap这个名字取的真的很好,如题Treap=Tree+Heap!         这个数据结构的特点在于非常的简洁易懂,当然了,前提是你学好了其他的平衡树。它的代码量是公认比较少的,不敢说最少……下面说说它的实现方式。我们知道,一棵普通的BST由于存储方式固定,所

alpc_qleonardo 757

LDA模型参数优化实战:从auto设置到passes调优

本文深入探讨了LDA主题模型的参数优化实战,重点解析了如何通过设置alpha='auto'与eta='auto'让模型自适应语料,并详细阐述了passes参数对训练效果的核心影响。文章提供了系统的调优方法与评估策略,旨在帮助用户从“能用”到“用好”LDA,产出清晰、可解释的主题分析结果。

weixin_29163797的博客 117

告别龟速!实测Copernicus Dataspace下载Sentinel-2数据的3个提速技巧(附Workspace批量操作)

本文分享了在Copernicus Dataspace平台高效下载Sentinel-2遥感数据的3个实用技巧,包括网络传输层优化、浏览器与API调优以及Workspace批量操作。通过这些方法,用户可将下载速度提升300%以上,特别适合科研人员和团队处理大规模数据需求。

weixin_30706691的博客 178

浅谈BST中的Splay

Ⅰ、预备知识 \(Splay\),二叉查找树(BST,Binary Search Tree)的一种。 其性质满足中序遍历是严格上升的,即左子树中所有数的键值<根节点的键值<右子树中所有数的键值,这一特点,一般支持一下几种操作: 1、插入一个权值为v的数 2、删除一个权值为v的数 3、查询v数的排名 4、查询排名为x的数 5、求v的前驱 6、求v的后继 类似能实现这些操作的BST还有比如...

weixin_30703911的博客 194

Klocwork安装

简单介绍一下Klocwork在windows下的安装,操作系统是win7,Klocwork的版本是10.0。 第一步:由于Klocwork的安装程序已十分成熟,所以在安装之前不需要什么准备共走。双击Klocwork的安装包,会出现下图的安装引导界面,点击Next按钮。 第二步:下图是协议界面,点击Next按钮。 第三步:选择Klocwork的安装目录,点击Next按钮 第四步:

旮旯 9927

【模板篇】splay(填坑)+模板题(普通平衡树)

划着划着水一不小心NOIP还考的凑合了… 所以退役的打算要稍微搁置一下了… 要准备准备省选了…. 但是自己已经啥也不会了… 所以只能重新拾起来… 从splay开始吧… splay我以前扔了个板子来着, 之前理解的还是不够深入于是就一直没有填坑… 现在又重新拾了一下就来填坑… 不过讲平衡树要画好多图的说OvO BST(二叉查找树)大家应该都知道吧, 所以就不讲了…...

weixin_30337251的博客 186

PCL 点云配准-ICP算法(精配准)

ICP(Iterative Closest Point)算法是一种常用的点云精细配准算法。它通过迭代计算将一组点云变换到另一组点云的最佳位置,最终实现精确的对齐。ICP算法在每次迭代中选择与目标点云最接近的点作为对应点,通过计算最小化源点云和目标点云之间的均方误差(MSE)来进行刚性变换(包括平移和旋转)。

纵马踏花向自由 1724

基于YOLO的智能视频监控异常行为检测研究:从算法原理到落地部署的全栈实践

本文深入探讨如何利用 YOLO(You Only Look Once)系列算法构建一套端到端的异常行为检测系统。文章首先回顾 YOLOv8 在时空建模上的局限,然后提出“YOLO-STA(Spatio-Temporal Aggregation)”轻量级改进框架,在公开数据集 UCF-Crime 上将异常行为检测的 mAP@0.5 从 72.4 提升到 80.1;接着给出完整训练、推理、部署的 Python 代码(PyTorch + TensorRT),并介绍如何仅用 4 GB 显存的 Jetson Xavi

博客 1318

【C++进阶】告别手撕红黑树的噩梦!从BST到神级数据结构FHQ Treap,万字图解彻底拿下!

还在纠结用哪种平衡树?平衡树种类核心思想优势适用场景学习难度替罪羊树暴力重构代码简单,无旋转静态或修改极少的场景⭐⭐普通Treap随机优先级+旋转常数小,速度快普通的查找、排名、前驱后继⭐⭐⭐FHQ Treap分裂 + 合并天下无敌!好写、能区间、能持久化99%的算法竞赛和面试场景⭐⭐⭐红黑树/AVL复杂的旋转分类极端严谨,最坏时间复杂度稳定系统底层实现 (如C++ map, Java TreeMap)⭐⭐⭐⭐⭐ (劝退)笛卡尔树单调栈线性建树OnO(n)On。

kisbad的博客 589

Vue3 - [兼容PC和手机H5] 详细监听浏览器刷新 / 关闭 / 前进后退事件,用户点击关闭和刷新页面前 “拦截“ 操作并弹出提示框(实时监听用户关闭或刷新网页,触发时文字提醒并执行自定义操作)

vue3监听页面关闭和刷新,vue3中如何监听页面刷新和关闭事件,vue3中监听浏览器窗口关闭事件,并在窗口关闭进行操作,vue3怎么只在某个页面监听,其他页面不监听,vue3监听刷新和关闭怎么离开页面时注销释放,vue3怎么阻止浏览器前进和后退并提示,页面跳转离开后依旧可以监听到刷新和关闭,vue3怎么知道用户关闭了网页,vue3如何监听刷新网页操作并截断,vue3移动端手机h5网页手机网站监听浏览器刷新关闭,vue3当页面关闭或刷新时怎么提示用户离开不保存数据,vue3项目监听浏览器窗口或标签页关闭,并

🏆 前端领域优质创作者 1万+

fhq-treap小结

终于AC fhq-Treap啦!!!撒花庆祝qwq 回头看看fhq-treap真的吼哇! 推一波大佬的博客:https://www.cnblogs.com/ppprseter/p/9382132.html 就是看这个学会的qwq 进入正题: fhq-treap,也具有Treap的性质,即堆+bst的性质,在此基础上,它不需要进行旋转以维护平衡,而是用拆树和合并的操作让树保持平衡 支持区...

weixin_34388207的博客 378

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

AcWing 253:普通平衡树 ← FHQ-Treap 树(结构体 + split + merge)

● FHQ-Treap 的高明之处在于所有的操作都只用到了分裂(split)与合并(merge)这两个基本操作。

hnjzsyjyj的专栏 1212

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

序——平衡树

平衡树,高级数据结构,维护前缀后继的有力工具,种类繁多。其中treap因其较为简单的代码编写,成为竞赛的主要平衡树之一;splay凭借强大的区间修改以及相对较小的代码量,成为另一种竞赛平衡树。 本章中,我们将陆续学习平衡树的前置——BST,Treap,Splay。当然,我们会涉猎FHQ。 未来还可能有更多种类的平衡树,如:替罪羊树,红黑树,SBT……有兴趣的读者可以上网查阅。 下...

weixin_30252709的博客 92

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

平衡树学习笔记

本文介绍了线段树和平衡树的基本原理与应用。线段树本质是基于排序的二分结构,通过不同排序对象实现不同功能。二叉查找树(BST)通过中序遍历实现有序存储,但可能退化为链式结构。平衡树(Treap)通过随机旋转优化BST的高度,保证操作效率。文章详细讲解了旋转规则(左旋/右旋)和平衡树的插入、删除、查询排名等核心操作,并以洛谷例题说明具体实现方法。平衡树结合了BST的排序特性和随机旋转策略,有效解决了普通BST可能退化为链表的问题,确保操作时间复杂度维持在O(log n)级别。

cwplh的博客 435

Treap(树堆)从入门到精通:旋转与无旋的实战抉择

本文深入解析了Treap(树堆)这一数据结构,对比了旋转式与无旋式(FHQ Treap)两种核心实现。旋转式Treap通过左旋/右旋保持平衡,代码直观高效;无旋式Treap基于分裂与合并操作,天生支持区间操作与可持久化。文章结合实战场景,为开发者在数据结构选型上提供了清晰的决策指南。

weixin_28365523的博客 544

FHQ-Treap:无旋Treap的极致优雅,从原理到区间操作实战

int l, r;bool rev;// 翻转懒标记} tr[N];return idx;FHQ-Treap是“无旋设计+随机化+懒标记”的完美结合——它用split和merge替代旋转,既保留了普通Treap“随机权值保平衡”的核心优势,又解决了区间操作的痛点,成为算法竞赛中处理动态序列问题的“通用解”。逻辑统一:所有操作均可拆解为“split-处理-merge”,无需记忆复杂的旋转规则;拓展性强:仅需新增懒标记即可支持区间加、区间求和等操作;学习成本低。

yanghb916的博客 647

平衡树核心学习笔记

平衡树是二叉搜索树(BST)的优化形态,核心目标是通过「平衡约束+修复机制」避免BST退化为链表,保证插入、删除、查询、区间操作等所有核心操作的时间复杂度稳定在Olog⁡nO(\log n)Ologn。本文新增红黑树(工程最常用平衡树),系统拆解8类主流平衡树的核心原理,详细讲解插入、删除、排名、前驱/后继、区间操作的完整思路,所有操作附文字可视化图解,无代码、无冗余内容,聚焦核心逻辑与落地思路。基础共识。

yanghb916的博客 686

有关平衡树

本文将非常详细地,从前置知识开始介绍平衡树,方便新手快速掌握FHQ-Treap

2509_93020071的博客 807

FHQ Treap 总结

【前言(一堆废话)】 目前 OI 竞赛中两大主流平衡树之一就是 FHQ Treap(另一个是 Splay)。 普通 BST 的中序遍历中,val 值构成一个单调递增的序列。 Treap 在 BST 的基础上,维护了一个 key 值,使其 val 满足 BST 性质,key 满足 Heap 性质。在维护 Heap 性质的同时,使 Treap 的树高期望是 log(n)。通常情况下,key 值是...

又又大柚纸的博客 6707

【模板】文艺平衡树 无旋FHQ Treap

本题并非 #include<iostream> #include<cstdio> #include<algorithm> #include<ctime> using namespace std; int n,leftn,rightn,m; struct node{ node *lson,*rson; int pri;int siz; int val;int mark; void getsize()//获取树的大小 { siz = 1;

Bell的博客 377

[BZOJ 3224]Tyvj 1728 普通平衡树

Description 您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作: 1. 插入x数 2. 删除x数(若有多个相同的数,因只删除一个) 3. 查询x数的排名(若有多个相同的数,因输出最小的排名) 4. 查询排名为x的数 5. 求x的前驱(前驱定义为小于x,且最大的数) 6. 求x的后继(后继定义为大于x,且最小的数) ...

weixin_30344995的博客 117
上一篇: AtCoder ABC166
下一篇: 2020.5.5集训总结
NephrenRuqInsania
博客等级 码龄8年 20粉丝 115原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值