前言

平衡树在我的心目中,一直都是一个很高深莫测的数据结构。不过,由于最近做的题目的题解中经常出现“平衡树”这三个字,我决定从最简单的替罪羊树开始,好好学习平衡树。

简介

替罪羊树,英文名\(Scapegoat\ Tree\),是我认为平衡树中最简单的一种。

替罪羊树可以当作一棵非常暴力二叉搜索树,因为它除了在子树不平衡时会暴力重构(不然为什么叫它平衡树)以外几乎和BST没有任何区别。

替罪羊树的基础操作

  • 插入

    不得不说,替罪羊树的插入操作简直与BST一模一样。

    直接上代码:

inline void Insert(int &x,int val)//插入操作
{
if(!x)//如果当前节点为空,那么就将元素插入这个节点
{
x=Void[tot--],node[x].Val=val,node[x].Exist=1,Build(x);
return;
}
++node[x].Size,++node[x].Fac;//将这个子树的大小加1
if(val<=node[x].Val) Insert(node[x].Son[0],val);//比较插入元素与当前元素,若小于等于当前元素,就插入到当前元素的左子树
else Insert(node[x].Son[1],val);//否则,就插入到当前元素的右子树
}
  • 删除

    替罪羊树的删除操作就很值得一提了。

    在删除替罪羊树上的一个元素时,我们并不会将其暴力删除(虽然替罪羊树在重构时非常暴力,但它的暴力是有选择性的,不然复杂度还不上天),而是标记这个节点不存在,并在计算它所在子树大小时将实际大小减1。这个思想是非常实用的,在许多地方我们都会用到。

    代码如下:

inline void Delete(int &x,int rk)//删除排名为rk的数
{
if(node[x].Exist&&!((node[node[x].Son[0]].Fac+1)^rk))//如果当前节点存在(没有被删除)且刚好排名为rk,我们就将其删除
{
node[x].Exist=0,--node[x].Fac;//标记其不存在,并将该子树的实际大小减1
return;
}
--node[x].Fac;//因为该子树中将有元素被删除,所以将该子树的实际大小减1
if(node[node[x].Son[0]].Fac+node[x].Exist>=rk) Delete(node[x].Son[0],rk);//比较删除元素与当前元素的大小,若小于等于当前元素,就说明要删除的元素在当前元素的左子树
else Delete(node[x].Son[1],rk-node[x].Exist-node[node[x].Son[0]].Fac);//否则说明要删除的元素在当前元素的右子树
}
inline void del(int v)//删除值为v的数
{
Delete(rt,get_rank(v));//删除值为v的数,就相当于删除排名为值为v的数的排名的数,是不是有点绕?
if((double)node[rt].Size*alpha>(double)node[rt].Fac) ReBuild(rt);//如果当前子树的实际大小小于该子树的大小乘以alpha(一般来说,取alpha=0.75),就重构该子树
}
  • 重构

    呃,接下来到了最关键的部分:重构。

    替罪羊树的重构真的是非常暴力。我们可以形象地理解它:

假设上图是一棵需要重构的子树(圆圈中是节点编号而不是节点权值)。

那么,我们就先非常暴力地将其拍扁:

然后,再将它以最中间的节点为新的根,重新拎起来:

重构就完成了。是不是一个极其暴力的过程?

代码如下:

inline void Traversal(int x)//拍扁原子树(中序遍历原子树,这样可以保证遍历后得到的元素是从大到小排序的)
{
if(!x) return;//如果当前节点是空节点,就退出函数
Traversal(node[x].Son[0]);//由于是中序遍历,所以先遍历该节点的左子树
if(node[x].Exist) cur[++cnt]=x;//如果该节点存在,就将其加入数组
else Void[++tot]=x;//否则删除该节点,将该节点加入存储空节点的数组,方便动态开点
Traversal(node[x].Son[1]);//最后遍历该节点的右子树
}
inline void SetUp(int l,int r,int &x)//将拍扁的树重新拎起(一个分治的操作)
{
int mid=l+r>>1;x=cur[mid];//将新的根节点设定为这段区间的中点(使重构出的树尽量平衡)
if(l==r)//如果这是一个叶子节点
{
Build(x);//重置该节点
return;//退出函数
}
if(l<mid) SetUp(l,mid-1,node[x].Son[0]);//如果当前元素左边还有数,说明它有左子树,重构它的左子树
else node[x].Son[0]=0;//否则它的左子树为空
SetUp(mid+1,r,node[x].Son[1]),PushUp(x);//重构它的右子树
}
inline void ReBuild(int &x)//重构的过程
{
cnt=0,Traversal(x);//拍扁
if(cnt) SetUp(1,cnt,x);//拎起
else x=0;//特判该子树为空的情况
}
  • 询问

    这应该是替罪羊树中最后一个比较基础的操作了。

    作为一棵升级版的BST,它的功能与BST差不多:询问值为\(v\)的数的排名排名为\(rk\)的数的值

    查询过程也与BST差不多,只不过要多判断一些节点不存在的情况。

    代码如下:

inline int get_rank(int v)//询问值为v的数的排名
{
int x=rt,rk=1;//初始化计数器为1(v本身)
while(x)//只要当前节点不为空
{
if(node[x].Val>=v) x=node[x].Son[0];//如果当前元素大于v,则说明当前元素的排名大于v,所以访问当前元素的左子树
else rk+=node[node[x].Son[0]].Fac+node[x].Exist,x=node[x].Son[1];//否则,将计数器加上当前元素的排名,并访问当前元素的右子树
}
return rk;
}
inline int get_val(int rk)//询问排名为rk的数的值
{
int x=rt;
while(x)//只要当前节点不为空
{
if(node[x].Exist&&node[node[x].Son[0]].Fac+1==rk) return node[x].Val;//如果当前元素的排名等于rk,则返回该节点的值
else if(node[node[x].Son[0]].Fac>=rk) x=node[x].Son[0];//否则,如果当前元素的排名大于rk,访问当前元素的左子树
else rk-=node[x].Exist+node[node[x].Son[0]].Fac,x=node[x].Son[1];//不然,就将rk减去当前元素的排名,访问当前元素的右子树
}
}

完整代码

讲了这么多,最后来一个模板:(以【洛谷3369】【模板】普通平衡树为例)

#include<bits/stdc++.h>
#define N 100000
using namespace std;
int n,st,rt,cnt,tot,cur[N+5],Void[N+5];
const double alpha=0.75;
struct Scapegoat
{
int Son[2],Exist,Val,Size,Fac;
}node[N+5];
inline char tc()
{
static char ff[100000],*A=ff,*B=ff;
return A==B&&(B=(A=ff)+fread(ff,1,100000,stdin),A==B)?EOF:*A++;
}
inline void read(int &x)
{
x=0;int f=1;char ch;
while(!isdigit(ch=tc())) if(ch=='-') f=-1;
while(x=(x<<3)+(x<<1)+ch-'0',isdigit(ch=tc()));
x*=f;
}
inline void write(int x)
{
if(x<0) putchar('-'),x=-x;
if(x>9) write(x/10);
putchar(x%10+'0');
}
inline void Init()
{
tot=0;
for(register int i=N-1;i;--i) Void[++tot]=i;
}
inline bool balance(int x)
{
return (double)node[x].Fac*alpha>(double)max(node[node[x].Son[0]].Fac,node[node[x].Son[1]].Fac);
}
inline void Build(int x)
{
node[x].Son[0]=node[x].Son[1]=0,node[x].Size=node[x].Fac=1;
}
inline void Insert(int &x,int val)
{
if(!x)
{
x=Void[tot--],node[x].Val=val,node[x].Exist=1,Build(x);
return;
}
++node[x].Size,++node[x].Fac;
if(val<=node[x].Val) Insert(node[x].Son[0],val);
else Insert(node[x].Son[1],val);
}
inline void PushUp(int x)
{
node[x].Size=node[node[x].Son[0]].Size+node[node[x].Son[1]].Size+1,node[x].Fac=node[node[x].Son[0]].Fac+node[node[x].Son[1]].Fac+1;
}
inline void Traversal(int x)
{
if(!x) return;
Traversal(node[x].Son[0]);
if(node[x].Exist) cur[++cnt]=x;
else Void[++tot]=x;
Traversal(node[x].Son[1]);
}
inline void SetUp(int l,int r,int &x)
{
int mid=l+r>>1;x=cur[mid];
if(l==r)
{
Build(x);
return;
}
if(l<mid) SetUp(l,mid-1,node[x].Son[0]);
else node[x].Son[0]=0;
SetUp(mid+1,r,node[x].Son[1]),PushUp(x);
}
inline void ReBuild(int &x)
{
cnt=0,Traversal(x);
if(cnt) SetUp(1,cnt,x);
else x=0;
}
inline void check(int x,int val)
{
int s=val<=node[x].Val?0:1;
while(node[x].Son[s])
{
if(!balance(node[x].Son[s]))
{
ReBuild(node[x].Son[s]);
return;
}
x=node[x].Son[s],s=val<=node[x].Val?0:1;
}
}
inline int get_rank(int v)
{
int x=rt,rk=1;
while(x)
{
if(node[x].Val>=v) x=node[x].Son[0];
else rk+=node[node[x].Son[0]].Fac+node[x].Exist,x=node[x].Son[1];
}
return rk;
}
inline int get_val(int rk)
{
int x=rt;
while(x)
{
if(node[x].Exist&&node[node[x].Son[0]].Fac+1==rk) return node[x].Val;
else if(node[node[x].Son[0]].Fac>=rk) x=node[x].Son[0];
else rk-=node[x].Exist+node[node[x].Son[0]].Fac,x=node[x].Son[1];
}
}
inline void Delete(int &x,int rk)
{
if(node[x].Exist&&!((node[node[x].Son[0]].Fac+1)^rk))
{
node[x].Exist=0,--node[x].Fac;
return;
}
--node[x].Fac;
if(node[node[x].Son[0]].Fac+node[x].Exist>=rk) Delete(node[x].Son[0],rk);
else Delete(node[x].Son[1],rk-node[x].Exist-node[node[x].Son[0]].Fac);
}
inline void del(int v)
{
Delete(rt,get_rank(v));
if((double)node[rt].Size*alpha>(double)node[rt].Fac) ReBuild(rt);
}
int main()
{
for(read(n),Init();n;--n)
{
int op,x;read(op),read(x);
switch(op)
{
case 1:st=rt,Insert(rt,x),check(st,x);break;
case 2:del(x);break;
case 3:write(get_rank(x)),putchar('\n');break;
case 4:write(get_val(x)),putchar('\n');break;
case 5:write(get_val(get_rank(x)-1)),putchar('\n');break;
case 6:write(get_val(get_rank(x+1))),putchar('\n');break;
}
}
return 0;
}

简析平衡树(一)——替罪羊树 Scapegoat Tree的更多相关文章

  1. 简析平衡树(三)——浅谈Splay

    前言 原本以为\(Treap\)已经很难了,学习了\(Splay\),我才知道,没有最难,只有更难.(强烈建议先去学一学\(Treap\)再来看这篇博客) 简介 \(Splay\)是平衡树中的一种,除 ...

  2. 简析平衡树(四)——FHQ Treap

    前言 好久没码过平衡树了! 这次在闪指导的指导下学会了\(FHQ\ Treap\),一方面是因为听说它可以可持久化,另一方面则是因为听说它是真的好写. 简介 \(FHQ\ Treap\),又称作非旋\ ...

  3. 简析平衡树(二)——Treap

    前言 学完了替罪羊树,我决定再去学一学\(Treap\).一直听说\(Treap\)很难,我也花了挺久才学会. 简介 \(Treap\)这个名字真的挺有内涵: \(\color{red}{Tree}\ ...

  4. 平衡树 替罪羊树(Scapegoat Tree)

    替罪羊树(Scapegoat Tree) 入门模板题 洛谷oj P3369 题目描述 您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作: 插入xx数 删除xx数(若有多个相同 ...

  5. [模板] 平衡树: Splay, 非旋Treap, 替罪羊树

    简介 二叉搜索树, 可以维护一个集合/序列, 同时维护节点的 \(size\), 因此可以支持 insert(v), delete(v), kth(p,k), rank(v)等操作. 另外, prev ...

  6. 在平衡树的海洋中畅游(二)——Scapegoat Tree

    在平衡树的广阔天地中,以Treap,Splay等为代表的通过旋转来维护平衡的文艺平衡树占了觉大部分. 然而,今天我们要讲的Scapegoat Tree(替罪羊树)就是一个特立独行的平衡树,它通过暴力重 ...

  7. 【bzoj3224】Tyvj 1728 普通平衡树 01Trie姿势+平衡树的四种姿势 :splay,旋转Treap,非旋转Treap,替罪羊树

    直接上代码 正所谓 人傻自带大常数 平衡树的几种姿势:  AVL Red&Black_Tree 码量爆炸,不常用:SBT 出于各种原因,不常用. 常用: Treap 旋转 基于旋转操作和随机数 ...

  8. 平衡树简单教程及模板(splay, 替罪羊树, 非旋treap)

    原文链接https://www.cnblogs.com/zhouzhendong/p/Balanced-Binary-Tree.html 注意是简单教程,不是入门教程. splay 1. 旋转: 假设 ...

  9. 平衡树及笛卡尔树讲解(旋转treap,非旋转treap,splay,替罪羊树及可持久化)

    在刷了许多道平衡树的题之后,对平衡树有了较为深入的理解,在这里和大家分享一下,希望对大家学习平衡树能有帮助. 平衡树有好多种,比如treap,splay,红黑树,STL中的set.在这里只介绍几种常用 ...

随机推荐

  1. ARC085F(动态规划,线段树)

    #include<bits/stdc++.h>using namespace std;const int maxn = 0x3f3f3f3f;int mn[801000];int cost ...

  2. redhat Enterprise Linux 6 VNC安装

    redhat Enterprise Linux 6.2 beta VNC安装经验  VNC(Virtual Network Computing)是可操控远程的计算机的软件,任何人都可免费取得该软件,其 ...

  3. d190305面试01过程记录和总结(java开发)

    1.签到,做题,注意时间.(疑问:做笔试题的时候,没有人监管吗?作弊怎么办) 2.今天做的是比较基础的题,题型有判断题(8),选择题(10),简答题(3),编程(2)逻辑题(2) (ps:感觉做的还不 ...

  4. @Inherited:允许子类继承父类的注解。

    在看定义注解的相关文章的时候,看到这个@Inherited注解,简单的说明并没有真正搞懂是什么意思.在网上搜索了一些相关的内容,现在把一篇文章转载过来.以便后面使用. 文章出处,转载地址:(http: ...

  5. HDU 5773 The All-purpose Zero 脑洞LIS

    给定一个序列,里面的0是可以任变的.问变化后最长的LIS的长度 首先,0全部选上是不亏的.这个不知道怎么说,YY一下吧. 最关键的就是解决2 0 0 3 这种问题了. 注意到这个序列的LIS应该是3 ...

  6. ElasticSearch 全文检索— ElasticSearch 安装部署

    ElasticSearch 规划-集群规划 ElasticSearch 规划-集群规划 ElasticSearch 规划-用户规划 ElasticSearch 规划-目录规划 ElasticSearc ...

  7. VMware下linux与window文件夹共享

    这里说的是在虚拟机下来实现在windows下共享一个文件夹. 下面来说明一下是如何实现的: 1.  安装VMware.Workstation. 2.  安装Redhat Linux 9.0,在虚拟机下 ...

  8. 关于wav文件fft处理后x,y轴坐标数据的问题

    1.关于横坐标的频率的最大值是采样频率,那么每个点对应的频率值就很好算了:f(n) = [Fs/(N/2)]*n  (Fs是采样频率,常见的是44.1KHz(44100),N是采样点数,k表是第k个点 ...

  9. springcloud中servcie层调用fegin异常以及异步方法的实现

    近日在做业务上的短信推送和APP消息推送,通过调用别的模块的接口来实现,在springcloud中通过fegin进行调用.这里要说明的事情并不是如何开发推送功能,而是在调试过程中碰到的一些小问题.我把 ...

  10. Linux查找文件内容(grep)

    转载链接:http://www.eguidedog.net/linux-tutorial/05-grep.php grep是Linux命令行下常用于查找过滤文本文件内容的命令.最简单的用法是: gre ...