首页 > 其他分享 >Treap(平衡树)

Treap(平衡树)

时间:2024-01-15 16:12:06浏览次数:29  
标签:return val get int Treap key 平衡 节点

Treap

前置芝士

二叉搜索树(BST),见 BST
平衡二叉树(AVL)。

先来介绍一下平衡二叉树。

背景

BST 出现以后,人们很快发现一个问题,当其维护一个有序序列时,很可能会退化成链。如图:

img
这样的话,原来 \(O(\log{n})\) 的复杂度就退化为 \(O(n)\),这是我们无法接受的,于是平衡二叉树横空出世。

定义

平衡二叉树:左右子树的高度相差不超过 1 的 BST(可以为空树)。平衡,顾名思义,就是要求左右子树的高度相近。
下面给出一些图,请判断是否为平衡二叉树:
img
img
img
显然,只有第二棵树是平衡二叉树,第一棵树节点 5 左右子树不平衡,第三棵树不是 BST。

基础知识

treap,就是“树堆”,由树和堆组成,是一种入门级的平衡二叉树,操作较多,码量较大,但比较基础,好理解。

\[Treap=tree+heap \]

二叉搜索树(BST)对于一个序列来说不唯一,也就是说,在满足“BST性质”的前提下,中序遍历为相同序列的 BST 不唯一。因此,在 BST 的基础上加上二叉堆,来保证平衡性。用来维护 BST 的值为“关键码”,维护堆的值为权值,权值是随机产生的,避免退化。维护堆性质的操作为“旋转”。Treap 是一种通过适当的旋转,在维持节点关键码满足 BST 性质的同时,还使每个节点上随机生成的权值满足二叉堆的性质的平衡二叉树。 各个操作时间复杂度为 \(O(\log{n})\)。
基本操作有:

  1. 插入一个数 \(x\)。
  2. 删除一个数 \(x\)(若有多个相同的数,应只删除一个)。
  3. 定义排名为比当前数小的数的个数 \(+1\)。查询 \(x\) 的排名。
  4. 查询数据结构中排名为 \(x\) 的数。
  5. 求 \(x\) 的前驱(前驱定义为小于 \(x\),且最大的数)。
  6. 求 \(x\) 的后继(后继定义为大于 \(x\),且最小的数)。

操作

建树

与 BST 相同,建立一棵空树,不过我们需要储存更多的信息,size 为以该节点为根的子树大小,cnt 表示序列中该关键字的个数,pushup 和线段树的一样,更新父亲的信息。

const int N=1e5+5,inf=1<<30;

struct treap
{
    int l,r;
    int key,dat;//关键字,附加权值
    int size,cnt;//子树大小,副本数
}a[N];
int tot,root,n,m;

int New(int k)
{
    a[++tot].key=v;
    a[tot].dat=rand();
    a[tot].cnt=a[tot].size=1;
    return tot;
}
void pushup(int u)
{
    a[u].size=a[a[u].l].size+a[a[u].r].size+a[u].cnt;
}
void build()
{
    New(-inf),New(inf);
    root=1,a[root].r=2;
}

旋转

旋转是 Treap 的基本操作,分为左旋和右旋。如图:

img

  • 右旋:把父亲变为左儿子的右儿子。
  • 左旋:把父亲变为右儿子的左儿子。

如图,对于黑色节点来说,左边右旋后,该节点的左子树节点数减一,右子树加一,右边左旋后,该节点的左子树节点树加一,右子树减一。也就是说对于一个节点右旋,会增加右子树的节点数,左旋会增加左子树的节点数,利用左旋和右旋我们就可以维护二叉平衡树。

以右旋为例,将 \(y\) 变为 \(x\) 的右儿子,对于 \(x\) 左儿子不变,右儿子变为 \(y\),这样 \(y\) 的左儿子就空出来了,刚好可以把 \(x\) 的右子树 \(B\) 接上去。具体:\(y\) 的左子树变为 \(B\),\(x\) 的右儿子变为 \(y\),\(x\) 代替 \(y\) 原来的位置。

左旋同理:

void zig(int &u)//右旋
{
    int q=a[u].l;
    a[u].l=a[q].r,a[q].r=u,u=q;
    pushup(a[u].r),pushup(u);
}
void zag(int &u)//左旋
{
    int q=a[u].r;
    a[u].r=a[q].l,a[q].l=u,u=q;
    pushup(a[u].l),pushup(u);
}

当权值不满足大根堆(小也可)性质时,交换父亲和儿子,这样就能使 BST 更平衡。
注意要用 &,这样的话会一起更新它父亲的信息,可以认为,\(q\) 完全代替了 \(u\)。

插入

将 \(key\) 为 \(v\) 的数插入到平衡树中,若原本已经存在,则直接找到位置将 \(cnt\) 加上 1。若不存在,则需将这个数插入到叶子节点,一直向下走直到找到要插入的位置,插入后判断是否满足大根堆性质,进行旋转来维护。

void insert(int &u,int v)
{
    if(u==0) u=New(v);//u指向新的节点
    else if(a[u].key==v) a[u].cnt++;
    else if(a[u].key>v) 
    {
        insert(a[u].l,v);
        if(a[u].dat<a[a[u].l].dat) zig(u);//不满足大根堆,交换左儿子和父亲
    }
    else 
    {
        insert(a[u].r,v);
        if(a[u].dat<a[a[u].r].dat) zag(u);//不满足大根堆,交换右儿子和父亲
    }
    pushup(u);
}

删除

将 \(key\) 为 \(v\) 的数从平衡树中删除,与插入类似,先要找到该节点。若该节点的 \(cnt\) 大于 1,直接减去 1 即可。若小于 1,考虑如何删除,如果该点是叶子节点那就好办了,直接用 0 代替,但如果不是,我们也不能直接删,要通过不断地旋转把它变成叶子节点,具体看代码,自己手推一遍就理解了。

void remove(int &u,int v)
{
    if(u==0) return;//没有这个数
    if(a[u].key==v) 
    {
        if(a[u].cnt>1) a[u].cnt--;
        else if(a[u].l&&a[u].r)//非叶子节点 
        {
            //保证旋转过后能满足大根堆的性质,哪个大就把哪个作为父亲
            if(a[u].r==0||a[a[u].l].dat>a[a[u].r].dat) zig(u),remove(a[u].r,v);//右旋后父亲变为右儿子
            else zag(u),remove(a[u].l,v)
        }
        else u=0;
    }
    else a[u].key>v ? remove(a[u].l,v) : remove(a[u].r,v);//一直往下走
    pushup(u);
}

查排名

\(v\) 的排名为小于它的个数 \(+1\)。考虑 BST 中,比当前节点小的点应该全部位于左子树,因此排名就是左子树的大小 \(+1\)。所以先找到该值,再算个数。考虑如何从根节点往下走:

  • \(key==v\),说明已找到,直接返回左子树的大小加 1。
  • \(key>v\),则需往左子树走。
  • \(key<v\),则需往右子树走,同时加上左子树大小和该节点副本数。
  • \(u==0\),说明树中不存在 \(v\) 的节点,直接加 1。

需要在递归出口加上 1。

int get_rank(int u,int v)
{
    if(u==0) return 1;
    if(a[u].key==v) return a[a[u].l].size+1;
    if(a[u].key>v) return get_rank(a[u].l,v);
    return get_rank(a[u].r,v)+a[a[u].l].size+a[u].cnt;
}

查值

已知排名为 \(k\),查询具体的值。大同小异:

  • 若左子树大小大于等于 \(k\),说明 \(k\) 在左子树,往左子树走。
  • 若 \(k\) 大于左子树大小且小于左子树大小加副本数,说明该节点就是答案。
  • 若 \(k\) 大于左子树大小加副本数,说明在右子树,往右子树继续找。
int get_val(int u,int k)
{
    if(a[a[u].l].size>=k) return get_val(a[u].l,k);
    if(a[a[u].l].size+a[u].cnt>=k) return a[u].key;
    return get_val(a[u].r,k-a[a[u].l].size-a[u].cnt);//减去比k小的值的个数
}

查前驱/后继

前驱定义为小于 \(x\),且最大的数,后继定义为大于 \(x\),且最小的数。
以前驱为例,一直往下走,不满足 \(key<x\) 则往左子树走,否则开始找最大值。

int get_pre(int u,int v)
{
    if(u==0) return -inf;
    if(a[u].key>=v) return get_pre(a[u].l,v);
    return max(a[u].key,get_pre(a[u].r,v));
}
int get_ne(int u,int v)
{
    if(u==0) return inf;
    if(a[u].key<=v) return get_ne(a[u].r,v);
    return min(a[u].key,get_ne(a[u].l,v));
}

P3369 【模板】普通平衡树

分析

将以上讲的所有操作结合在一起就好了,注意细节。

code

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define lc a[u].l
#define rc a[u].r
#define val a[u].key
const int N=1e5+5,inf=1<<30;

struct treap
{
    int l,r;
    int key,dat;
    int size,cnt;
}a[N];
int tot,root,n,m;

int New(int v)
{
    a[++tot].key=v;
    a[tot].dat=rand();
    a[tot].cnt=a[tot].size=1;
    return tot;
}
void pushup(int u)
{
    a[u].size=a[lc].size+a[rc].size+a[u].cnt;
}
void build()
{
    New(-inf),New(inf);
    root=1,a[root].r=2;
}
void zig(int &u)//右旋
{
    int q=lc;
    lc=a[q].r,a[q].r=u,u=q;
    pushup(rc),pushup(u);
}
void zag(int &u)//左旋
{
    int q=rc;
    rc=a[q].l,a[q].l=u,u=q;
    pushup(lc),pushup(u);
}
void insert(int &u,int v)
{
    if(u==0) u=New(v);
    else if(val==v) a[u].cnt++;
    else if(val>v)
    {
        insert(lc,v);
        if(a[lc].dat>a[u].dat) zig(u);//不满足大根堆,交换左儿子和父亲
    }
    else 
    {
        insert(rc,v);
        if(a[rc].dat>a[u].dat) zag(u);//交换右儿子
    }
    pushup(u);
}
void remove(int &u,int v)
{
    if(u==0) return ;
    if(val==v)
    {
        if(a[u].cnt>1) a[u].cnt--;
        else if(lc||rc) //有叶子节点
        {
            //保证旋转过后能满足大根堆的性质,哪个大就把哪个作为父亲
            if(rc==0||a[lc].dat>a[rc].dat) zig(u),remove(rc,v);//右旋后父亲变为右儿子
            else zag(u),remove(lc,v);
            pushup(u);
        }
        else u=0;
    }
    else val>v ? remove(lc,v) : remove(rc,v);
    pushup(u);
}
int get_rank(int u,int v)
{
    if(u==0) return 1;
    if(val==v) return a[lc].size+1;
    if(val>v) return get_rank(lc,v);
    return get_rank(rc,v)+a[lc].size+a[u].cnt;
}
int get_val(int u,int k)
{
    if(a[lc].size>=k) return get_val(lc,k);
    if(a[lc].size+a[u].cnt>=k) return val;
    return get_val(rc,k-a[lc].size-a[u].cnt);
}
int get_pre(int u,int v)
{
    if(u==0) return -inf;
    if(val>=v) return get_pre(lc,v);
    return max(val,get_pre(rc,v));
}
int get_ne(int u,int v)
{
    if(u==0) return inf;
    if(val<=v) return get_ne(rc,v);
    return min(val,get_ne(lc,v));
}
int main ()
{
    cin>>m;
    build();
    for(int op,x;m--;)
    {
        cin>>op>>x;
        if(op==1) insert(root,x);
        else if(op==2) remove(root,x);
        else if(op==3) cout<<get_rank(root,x)-1<<"\n";//减去-inf的点
        else if(op==4) cout<<get_val(root,x+1)<<"\n";//存在-inf
        else if(op==5) cout<<get_pre(root,x)<<"\n";
        else cout<<get_ne(root,x)<<"\n";
    }
    return 0;
}

结语

有了 Treap,时间复杂度不会退化了,但是——这代码也太长了。而 FHQ_Treap 和 Splay 就会更好 QAQ。

标签:return,val,get,int,Treap,key,平衡,节点
From: https://www.cnblogs.com/zhouruoheng/p/17957276

相关文章

  • 第十二节:红黑树性质、相对平衡的原理、与AVL树的区别
    一.        二.        三.         !作       者:Yaopengfei(姚鹏飞)博客地址:http://www.cnblogs.com/yaopengfei/声     明1:如有错误,欢迎讨论,请勿谩骂^_^。声     明2:原创博客请在转载......
  • 二叉搜索树 & 平衡树学习笔记
    注意,这是一篇学习笔记。二叉搜索树定义空树是二叉搜索树若二叉搜索树的左子树非空,左子树内每个点的权值均小于二叉搜索树根节点的权值若二叉搜索树的右子树非空,右子树内每个点的权值均大于二叉搜索树根节点的权值二叉搜索树的左右子树为二叉搜索树二叉搜索树中每个节......
  • 代码随想录 day17 平衡二叉树 二叉树的所有路径 左叶子之和
    平衡二叉树之前一直写迭代代码没有怎么写递归正好这题不是很好写迭代练习一下递归这题递归逻辑相对简单左右子树高度差判断是不是大于一可以直接返回结果不大于一就高度max(l,r)+1二叉树的所有路径关键要点这题适合先序遍历回溯过程和递归过程是一起写的进来几次......
  • 元学习与人工智能伦理的关系:如何平衡效率与道德
    1.背景介绍元学习(Meta-learning)是一种学习如何学习的学习方法,它旨在帮助机器学习模型在新任务上更快地学习。在过去的几年里,元学习已经取得了显著的进展,并在多个领域得到了广泛应用,如自然语言处理、计算机视觉和推荐系统等。然而,随着元学习在实际应用中的普及,人工智能社区开始关注......
  • 武汉灰京文化:游戏中如何平衡社交心理与娱乐体验
    一款游戏的魅力在于其带给用户的娱乐体验以及带给用户的无限想象空间。然而,作为一个游戏研发者,我们不仅要考虑游戏的体验感、吸引力和创新性,还要考虑玩家在游戏中的心理需求。其中,社交心理占据了重要的位置。玩家们在追求娱乐体验的同时,希望能够从游戏中结交新朋友,收获友情经验,解决......
  • 信息论与人工智能的伦理问题: 如何平衡利益与风险
    1.背景介绍信息论与人工智能的伦理问题是近年来随着人工智能技术的快速发展而引起的一个重要话题。随着数据、算法和计算能力的不断发展,人工智能技术已经成为了许多领域的重要驱动力,例如医疗诊断、金融风险管理、自动驾驶等。然而,随着人工智能技术的广泛应用,也引发了一系列伦理问题......
  • 代码随想录算法训练营第十七天 | 110.平衡二叉树,257. 二叉树的所有路径,404.左叶子之和
    一、110.平衡二叉树题目链接:LeetCode110.平衡二叉树学习:思路:后序遍历。实际上是由叶结点到根结点,若有一颗子树不是平衡二叉树,则直接返回给根结点二、257.二叉树的所有路径题目链接:LeetCode257.二叉树的所有路径学习:思路:递归+回溯。因为是线=先遍历根结点,然后遍历左孩......
  • [LeetCode Hot 100] LeetCode110. 平衡二叉树
    题目描述思路LeetCode104.二叉树的最大深度变种方法一:后序遍历(递归、dfs)/***Definitionforabinarytreenode.*publicclassTreeNode{*intval;*TreeNodeleft;*TreeNoderight;*TreeNode(){}*TreeNode(intval){this.......
  • nginx平衡升级在线升级
    nginx在线升级:nginx根据安装方式不同,升级方式也不同,一般有三种:yum方式安装,通过yum升级,不停机升级。编译方式安装,通过编译方式升级,不停机升级。容器方式安装,启动新容器方式升级,如果端口不变,需要停机,否则容器启动提示端口冲突。yum升级1、首选查看yum安装的nginx版本,nginx-V查看版本......
  • 1、平衡与非平衡、定阻与定压
    一、平衡与非平衡    平衡线和非平衡线组成 接线方式:平衡线  Hot----+    、cold-----、G---G进行连接非平衡线因为没有cold,    所以-和接地G当成一根连接,hot----+ 平衡和非平衡信号的传输过程:1、音源是以波的形式进行传输的,非平衡信号在......