平衡树解析

转自:yyb巨佬的平衡树

不知道splay是啥,,你也要知道平衡树是啥。。。

平衡树是一个神奇的数据结构,

对于任意一个节点,左儿子的值比它小,右儿子的值比它大

并且任意一棵子树单独拎出来也是一棵平衡树

就像这样。。。。

各位大佬请原谅我丑陋无比的图

上面这个丑陋的东西就是一棵平衡树,他现在很平衡,是一棵满二叉树,高度正好是logn。。。

但是。。

如果这个丑陋的东西极端一点,他就会变成这样。。。

这张图依然很丑

现在看起来,这个东西一点都不平衡。。。

二叉树退化成了一条链

如果要查询的话,,,最坏情况下就变成了O(n)

这就很尴尬了。。。



各位大佬们为了解决平衡树这个尴尬的问题,想出了各种方法。。

也就是弄出了各种树。。。。(然而cj大佬都会)

然后有一个注明的大佬叫做Tarjan,弄出了splay这个玩意。。。



这个玩意怎么解决上面的问题呢???

你是一个平衡树是吧。。。

我把你的节点的顺序修改一下,让你还是一棵平衡树,在这个过程中你的结构就变化了,就可能不再是一条链了。

诶,这个看起来很厉害的感觉。。。

但是,,我怎么说也说不清呀。。

弄张丑陋的图过来

这是一个丑陋的平衡树的一部分

其中XYZ三个是节点,ABC三个是三棵子树

现在这个玩意,我如果想把X弄到Y那个地方去要怎么办,这样的话我就经过了旋转,重构了这棵树的结构,就可能让他变得更加平衡

恩,我们来看看怎么办。。。

X是Y的左儿子,所以X < Y

Y是Z的左儿子,所以Y < Z

所以X < Z,所以如果要把X弄到Y的上面去的话,X就应该放到Y的那个位置

继续看,现在Y > X那么Y一定是X的右儿子

但是X已经有了右儿子B,

根据平衡树我们可以知道X < B < Y

所以我们可以把X的右儿子B丢给Y当做左儿子

而X的左儿子A有A < X < Y < Z显然还是X的左儿子

综上,我们一顿乱搞,原来的平衡树被我们搞成了这个样子

在检查一下

原来的大小关系是

A < X < B < Y < C < Z

把X旋转一下之后大小关系

A < X < B < Y < C < Z

诶,大小关系也没有变

所以之前那棵平衡树就可以通过旋转变成这个样子

并且这个时候还是一棵平衡树

好神奇诶。。。

但是,XYZ的关系显然不仅仅只有这一种

有Y是Z的左儿子 X是Y的左儿子

有Y是Z的左儿子 X是Y的右儿子

有Y是Z的右儿子 X是Y的左儿子

有Y是Z的右儿子 X是Y的右儿子

一共4种情况,大家可以自己画画图,转一转。



如果把上面的图画完了,我们就可以正式的来玩一玩splay了

转完了上面四种情况,我们来找找规律

最明显的一点,我们把X转到了原来Y的位置

也就是说,原来Y是Z的哪个儿子,旋转之后X就是Z的哪个儿子

继续看一看

我们发现,X是Y的哪个儿子,那么旋转完之后,X的那个儿子就不会变

什么意思?

看一看我上面画的图

X是Y的左儿子,A是X的左儿子,旋转完之后,A还是X的左儿子

这个应该不难证明

如果X是Y的左儿子,A是X的左儿子

那么A < X < Y旋转完之后A还是X的左儿子

如果X是Y的右儿子,A是X的右儿子

那么A > X > Y 只是把不等式反过来了而已

再看一下,找找规律

如果原来X是Y的哪一个儿子,那么旋转完之后Y就是X的另外一个儿子

再看看图

如果原来X是Y的左儿子,旋转之后Y是X的右儿子

如果原来X是Y的右儿子,旋转之后Y是X的左儿子

这个应该也很好证明吧。。。

如果X是右儿子 X > Y,所以旋转后Y是X的左儿子

如果X是左儿子 Y > X,所以旋转后Y是X的右儿子

所以总结一下:

1.X变到原来Y的位置

2.Y变成了 X原来在Y的 相对的那个儿子

3.Y的非X的儿子不变 X的 X原来在Y的 那个儿子不变

4.X的 X原来在Y的 相对的 那个儿子 变成了 Y原来是X的那个儿子

啊,,,写出来真麻烦,用语言来写一下

其中t是树上节点的结构体,ch数组表示左右儿子,ch[0]是左儿子,ch[1]是右儿子,ff是父节点

oid rotate(int x)//X是要旋转的节点
{
    int y=t[x].ff;//X的父亲
    int z=t[y].ff;//X的祖父
    int k=t[y].ch[1]==x;//X是Y的哪一个儿子 0是左儿子 1是右儿子
    t[z].ch[t[z].ch[1]==y]=x;//Z的原来的Y的位置变为X
    t[x].ff=z;//X的父亲变成Z
    t[y].ch[k]=t[x].ch[k^1];//X的与X原来在Y的相对的那个儿子变成Y的儿子
    t[t[x].ch[k^1]].ff=y;//更新父节点
    t[x].ch[k^1]=y;//X的 与X原来相对位置的儿子变成 Y
    t[y].ff=x;//更新父节点
}

上面的代码用了很多小小小技巧

比如t[y].ch[1]==x

t[y].ch[1]是y的右儿子,如果x是右儿子,那么这个式子是1,否则是0,也正好对应着左右儿子

同样的k^1,表示相对的儿子,左儿子0^1=1 右儿子1^1=0

好了,这就是一个基本的旋转操作(别人讲的



继续看接下来的东西

现在考虑一个问题

如果要把一个节点旋转到根节点(比如上面的Z节点呢)

我们是不是可以做两步,先把X转到Y再把X转到Z呢?

我们来看一看

一个这样的Splay

把X旋转到Y之后

再接着把X旋转到Z之后

好了,这就是对X连着旋转两次之后的Splay,看起来似乎没有什么问题。

但是,我们现在再来看一看

原图中的Splay有一条神奇链: Z->Y->X->B

然后再来看一看旋转完之后的Splay

也有一条链X->Z->Y->B

也就是说

如果你只对X进行旋转的话,

有一条链依旧存在,

如果是这样的话,splay很可能会被卡。

好了,

显然对于XYZ的不同情况,可以自己画图考虑一下,

如果要把X旋转到Z的位置应该如何旋转

归类一下,其实还是只有两种:

第一种,X和Y分别是Y和Z的同一个儿子

第二种,X和Y分别是Y和Z不同的儿子

对于情况一,也就是类似上面给出的图的情况,就要考虑先旋转Y再旋转X

对于情况二,自己画一下图,发现就是对X旋转两次,先旋转到Y再旋转到X

这样一想,对于splay旋转6种情况中的四种就很简单的分了类

其实另外两种情况很容易考虑,就是不存在Z节点,也就是Y节点就是Splay的根了

此时无论怎么样都是对于X向上进行一次旋转

那么splay的旋转也可以很容易的简化的写出来

void splay(int x,int goal)//将x旋转为goal的儿子,如果goal是0则旋转到根
{
    while(t[x].ff!=goal)//一直旋转到x成为goal的儿子
    {
        int y=t[x].ff,z=t[y].ff;//父节点祖父节点
        if(z!=goal)//如果Y不是根节点,则分为上面两类来旋转
            (t[z].ch[0]==y)^(t[y].ch[0]==x)?rotate(x):rotate(y);
            //这就是之前对于x和y是哪个儿子的讨论
        rotate(x);//无论怎么样最后的一个操作都是旋转x
    }
    if(goal==0)root=x;//如果goal是0,则将根节点更新为x
}

这样写多简单,比另外一些人写得分6种情况讨论要简单很多。



应SYC大佬要求,继续补充内容。



先是查找find操作

从根节点开始,左侧都比他小,右侧都比他大,

所以只需要相应的往左/右递归

如果当前位置的val已经是要查找的数

那么直接把他Splay到根节点,方便接下来的操作

类似于二分查找,

所以时间复杂度O(logn)

inline void find(int x)//查找x的位置,并将其旋转到根节点
{
    int u=root;
    if(!u)return;//树空
    while(t[u].ch[x>t[u].val]&&x!=t[u].val)//当存在儿子并且当前位置的值不等于x
        u=t[u].ch[x>t[u].val];//跳转到儿子,查找x的父节点
    splay(u,0);//把当前位置旋转到根节点
}

下一个Insert操作

往Splay中插入一个数

类似于Find操作,只是如果是已经存在的数,就可以直接在查找到的节点的进行计数

如果不存在,在递归的查找过程中,会找到他的父节点的位置,

然后就会发现底下没有啦。。。

所以这个时候新建一个节点就可以了

inline void insert(int x)//插入x
{
    int u=root,ff=0;//当前位置u,u的父节点ff
    while(u&&t[u].val!=x)//当u存在并且没有移动到当前的值
    {
        ff=u;//向下u的儿子,父节点变为u
        u=t[u].ch[x>t[u].val];//大于当前位置则向右找,否则向左找
    }
    if(u)//存在这个值的位置
        t[u].cnt++;//增加一个数
    else//不存在这个数字,要新建一个节点来存放
    {
        u=++tot;//新节点的位置
        if(ff)//如果父节点非根
            t[ff].ch[x>t[ff].val]=u;
        t[u].ch[0]=t[u].ch[1]=0;//不存在儿子
        t[tot].ff=ff;//父节点
        t[tot].val=x;//值
        t[tot].cnt=1;//数量
        t[tot].size=1;//大小
    }
    splay(u,0);//把当前位置移到根,保证结构的平衡
}

继续,,,

前驱/后继操作Next

首先就要执行Find操作

把要查找的数弄到根节点

然后,以前驱为例

先确定前驱比他小,所以在左子树上

然后他的前驱是左子树中最大的值

所以一直跳右结点,直到没有为止

找后继反过来就行了

inline int Next(int x,int f)//查找x的前驱(0)或者后继(1)
{
    find(x);
    int u=root;//根节点,此时x的父节点(存在的话)就是根节点
    if(t[u].val>x&&f)return u;//如果当前节点的值大于x并且要查找的是后继
    if(t[u].val<x&&!f)return u;//如果当前节点的值小于x并且要查找的是前驱
    u=t[u].ch[f];//查找后继的话在右儿子上找,前驱在左儿子上找
    while(t[u].ch[f^1])u=t[u].ch[f^1];//要反着跳转,否则会越来越大(越来越小)
    return u;//返回位置
}

还有操作呀/。。。

删除操作

现在就很简单啦

首先找到这个数的前驱,把他Splay到根节点

然后找到这个数后继,把他旋转到前驱的底下

比前驱大的数是后继,在右子树

比后继小的且比前驱大的有且仅有当前数

在后继的左子树上面,

因此直接把当前根节点的右儿子的左儿子删掉就可以啦

inline void Delete(int x)//删除x
{
    int last=Next(x,0);//查找x的前驱
    int next=Next(x,1);//查找x的后继
    splay(last,0);splay(next,last);
    //将前驱旋转到根节点,后继旋转到根节点下面
    //很明显,此时后继是前驱的右儿子,x是后继的左儿子,并且x是叶子节点
    int del=t[next].ch[0];//后继的左儿子
    if(t[del].cnt>1)//如果超过一个
    {
        t[del].cnt--;//直接减少一个
        splay(del,0);//旋转
    }
    else
        t[next].ch[0]=0;//这个节点直接丢掉(不存在了)
}

第K大数:从当前根节点开始,检查左子树大小

因为所有比当前位置小的数都在左侧

如果左侧的数的个数多余K,则证明第K大在左子树中

否则,向右子树找,找K-左子树大小-当前位置的数的个数

记住特判K恰好在当前位置

inline int kth(rg int x)//找第x小的数
{
    rg int now=root;//从根开始找
    if(ljl[now].size<x)return 0;//如果排名都超过总数了…………
    while(1)//嘿嘿,一直找
    {
        rg int ls=ljl[now].ch[0];//左孩子
        if(ljl[ls].size+ljl[now].cnt<x)//如果排名比左孩子总元素数和与我相等的数总和还大
        {
            x-=ljl[ls].size+ljl[now].cnt;//就减去前面的元素数
            now=ljl[now].ch[1];//去右孩子上找这个排名
        }
        else
            if(ljl[ls].size>=x)now=ls;//如果左孩子里包括它
            else return ljl[now].v;//那就在这个点上了,返回
    }
}

最后链接一下一些题目及题解

luoguP3369[模板]普通平衡树(Treap/SBT)     题解

luoguP3391[模板]文艺平衡树(Splay)               题解

原文地址:https://www.cnblogs.com/cjoierljl/p/8718931.html

时间: 2024-10-20 13:24:22

平衡树解析的相关文章

XJOI NOIP2015模拟赛Day1 T2 ctps bitset优化 或 排序+cdq分治+树状数组+平衡树

题意: 4维空间中有1个点集A,|A|=n,用(a,b,c,d)表示每个点. 共有m个询问,每次询问输入一个点(a,b,c,d),求最大的S,其中S={p|p∈A且ap<=a,bp<=b,cp<=c,dp<=d},输出|S| 输入格式: 第一行n 接下来n行有n个4维点对 第n+2行有一个数m 再接下来m行每行有一个四维点对,表示每个询问 输出格式: 对于每个询问输出一个数 **方法:**bitset优化 或 排序+cdq分治+树状数组+平衡树 解析: 神题,考场不会,暴力骗40,

二叉平衡树的插入和删除操作

1.      二叉平衡树 二叉排序树的时间复杂度和树的深度n有关.当先后插入的结点按关键字有序时,二叉排序树退化为单枝树,平均查找长度为(n+1)/2,查找效率比较低.提高查找效率,关键在于最大限度地降低树的深度n.因此需要在构成二叉排序树的过程中进行“平衡化”处理,使之成为二叉平衡树. 二叉平衡树,又称AVL树.它或者是一棵空树,或者是具有下列性质的树: 1)      具备二叉排序树的所有性质: 2)      左子树和右子树深度差的绝对值不超过1: 3)      左子树和右子树都是二叉

广告引擎解析

广告引擎 总体设计 检索服务 广告检索流程 广告定向 定向维度下的选项很少,可以枚举,这类定向包括,性别,年龄段,网络,系统. 我们的系统现在是如何处理这类定向的 定向维度有级联关系,省,市,区 定向维度是按照某个坐标附近N公里定向 检索服务是数据库的副本 CTR计算 计算二价 ADX广告 曝光服务 计费服务 计费服务主备 浮点数问题 广告引擎 总体设计 我们的基本架构是客户端请求API,然后由API发送RPC请求到我们的服务,服务通过注册中心来管理 检索服务,根据数据库中的广告建立索引,订阅R

BZOJ 1492 货币兑换 cdq分治或平衡树维护凸包

题意:链接 方法:cdq分治或平衡树维护凸包 解析: 这道题我拒绝写平衡树的题解,我仅仅想说splay不要写挂,insert边界条件不要忘.del点的时候不要脑抽d错.有想写平衡树的去看140142或者留言我. 首先这道题能推出个表达式 f[i]代表第i天最大收益. xx[i]表示将第i天的钱都买A的数量 yy[i]表示将第i天的钱都买B的数量 所以f[i]=max(f[i?1],p[i].a?xx[j]+p[i].b?yy[j])j<i 所以我们要维护这个n^2的递推式 又知道f[i]是由小于

MySQL重点内容:查询语句、名称解析

MariaDB安装方式:包管理器(rpm, deb)通用二进制格式:源码编译安装:SQL:数据库.表.索引.视图.存储过程.存储函数.触发器.事件调度器.用户和权限:元数据数据库:库名为mysqlMYSQL语句:有2种DDL, DMLDDL: CREATE, ALTER, DROPDML: INSERT, DELETE, UPDATE, SELECTDCL: GRANT(授权), REVOKE(撤销授权)MariaDB程序的组成:C/S架构C:Clientmysql:CLI交互式客户端程序:my

SkipList (跳跃表)解析及其实现

目录 导言 查找结点的效率如何提升? 什么是跳跃表? 跳跃表必须是完美的? 预备知识 抛硬币实验 模拟建表 操作解析 伪代码 代码实现 柔性数组 跳跃表的创建与销毁 跳跃表表头结构体定义 跳跃表结点结构体定义 建立跳跃表表头操作 操作解析 伪代码 代码实现 创建单个结点操作 操作解析 代码实现 销毁操作 操作解析 代码实现 插入操作 操作解析 时间复杂度 伪代码 代码实现 删除操作 操作解析 时间复杂度 伪代码 代码实现 查找操作 操作解析 伪代码 代码实现 时间复杂度分析 简单应用 跳跃字母表

C++工程编译之“error LNK2001: 无法解析的外部符号”

今天一整天都在折腾“error LNK2001: 无法解析的外部符号”,就在头疼不已的时候,总算是找到问题原因了:各个动态链接库的编译方式必须统一才行,要不然很容易对库函数的引用产生冲突.简单来说就是,如果使用的第三方函数库编译方式采用/MD,那么主工程也应该使用/MD.我使用了libevent,而主工程默认采用/MT,所以需要忽略一大堆的函数库,我还纳闷呢,怎么会这么奇怪!!今天总算是解决了长久以来的困惑了. 下面引用一篇文章的描述:[Z]VC运行库版本不同导致链接.LIB静态库时发生重复定义

防止恶意解析——禁止通过IP直接访问网站

一.什么是恶意解析 一般情况下,要使域名能访问到网站需要两步,第一步,将域名解析到网站所在的主机,第二步,在web服务器中将域名与相应的网站绑定.但是,如果通过主机IP能直接访问某网站,那么把域名解析到这个IP也将能访问到该网站,而无需在主机上绑定,也就是说任何人将任何域名解析到这个IP就能访问到这个网站.可能您并不介意通过别人的域名访问到您的网站,但是如果这个域名是未备案域名呢?一旦被查出,封IP.拔线甚至罚款的后果都是需要您来承担的.某些别有用心的人,通过将未备案域名解析到别人的主机上,使其

.NET深入解析LINQ框架(五:IQueryable、IQueryProvider接口详解)

阅读目录: 1.环路执行对象模型.碎片化执行模型(假递归式调用) 2.N层对象执行模型(纵横向对比链式扩展方法) 3.LINQ查询表达式和链式查询方法其实都是空壳子 4.详细的对象结构图(对象的执行原理) 5.IQueryable<T>与IQueryProvider一对一的关系能否改成一对多的关系 6.完整的自定义查询 1]. 环路执行对象模型.碎片化执行模型(假递归式调用) 这个主题扯的可能有点远,但是它关系着整个LINQ框架的设计结构,至少在我还没有搞懂LINQ的本意之前,在我脑海里一直频