替罪羊树(Scapegoat Tree)

入门模板题 洛谷oj P3369

题目

  您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:

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

输入格式

  第一行为n,表示操作的个数,下面n行每行有两个数opt和x,opt表示操作的序号( 1≤opt≤6 )

输出格式

  对于操作3,4,5,6每行输出一个数,表示对应答案

输入样例

10

1 106465
4 1
1 317721
1 460929
1 644985
1 84185
1 89851
6 81968
1 492737
5 493598

输出样例

106465

84185
492737

数据范围

1.n的数据范围: n≤100000

2.每个数的数据范围: [-10^7, 10^7]

  网上的资料比较琐碎难懂,之前看了很多资料一直不能理解平衡树(我太弱了)……前几天突然莫名其妙明白了,想写一篇笔记记录一下(乱写一通)。

0x00 二叉查找树

  要初步弄懂平衡树,首先要知道这是一棵二叉查找树

  二叉查找树(Binary Search Tree),当然也可以叫它二叉搜索树,或者二叉排序树(反正都一个意思都是二叉树),它的定义如下:

    或者是一棵空树,或者是具有下列性质的二叉树:

(1)若左子树不空,则左子树上所有结点的值均小于它的根节点的值;
(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)左、右子树也分别为二叉排序树;

    差不多就像下面这幅图一样:

0x01 平衡树的用途

  在学平衡树这个数据结构前,相信我们一定会先有个问题:平衡树能拿来干什么?

  网上的很多资料对这一点写得不太明白(也可能是我太弱了),我先试着乱总结一下:
    现在需要一种数据结构,它需要做到以下几点:
     1. 高效地查询一个序列中某个数的前面和后面的数(a[i-1]和a[i+1])。
     2. 高效地知道第i个数是什么(即a[i])。
     3. 高效地插入和删除。
  显然,我们可以用普通数组优秀地完成第1点和第2点,但第3点不能够了。当然,我们也可以用链表,复杂度O(1)优秀地完成第1点和第3点,但是对于第2点,链表的复杂度就达到了不太理想的O(n)。
  那我们能不能想办法优化一下链表呢?

  我们可以回头看一下二叉搜索树的定义,然后我们会发现,假设存在一个从1到8的链表(就像下图,win自带画图画的,不太好看)

  其实它也满足左边小于右边的定义,也可以勉强算是一棵以序号为每个节点的权值的二叉查找树。

  

  但是这样的一棵二叉树是不是很难看很畸形?
  我们怎么想办法把它变成一棵比较好看的树呢?
  可以拿笔画画看:
  我们尝试把中间的节点(4或者5,我选择4)拎出来,然后就变成了这样:

  

  是不是好看了一点?有点树的形状了,那我们可以尝试继续把两侧链上的中间节点继续拎出来,不断重复,最终会变成一棵比较好看的树。

  
  这就是平衡二叉树,严格遵守了二叉查找树的定义——左儿子小于右儿子。
  也许你会有疑问:长成这样的一棵树,怎么做到刚刚链表都不能完全做到的3点要求呢?
  让我们再看看这棵树:

0x02 查询前驱和后驱
  对于一个我们已知的节点i,我们先定义与它深度相同的都是它的兄弟节点。

  那么很显然,i的左兄弟及其子树上的所有节点都比i的左儿子及其子树上的所有节点来得小,且i的左儿子及其子树上的所有点都比i的父亲更大,所以显然,i的前驱要在i的左儿子及其子树上找。

  同理,仔细观察图,会发现我们所要找的前驱,存在于i的左儿子子树的最右侧,就是i的左儿子的右儿子的右儿子的右儿子的右儿子……(直到最后一个右儿子)

  这样,我们就可以O(log n)地求出i的前驱了。i的后驱同理。

0x03 查询第i个数

  前面说过,我们用数的序列编号作为节点的排序权值,所以我们只要像线段树那样从根部开始查一遍就可以了。详细解释:

  从根结点开始,如果第i个数比当前节点j的序号小,就往左儿子搜,反之右儿子。直到找到第i个数,时间复杂度还是O(log n)。

0x04 插入和删除
  插在原序列的末尾,所以新节点的编号是n+1。然后我们把这个节点变成根结点的右儿子的右儿子的右儿子……(变成最后一个没有右儿子的节点的右儿子)。

  删除。其实就是把i节点打个被删除掉的标记(甚至不打也可以)。然后让i的左右儿子之一成为i的父节点的新儿子(就是让某一个儿子取代i节点)。复杂度同O(log n)

  基本操作差不多就是这样。

  那么其实还有一个问题:如果操作太多,导致一棵本来平衡的树变成了一条链表,复杂度爆炸,怎么办呢?

0x05 重新建树
  如果你选择的是替罪羊树,那就是优雅的暴力了。替罪羊树在每个节点上记录子树的节点数size,同时还有一个平衡因子alpha(通常在0.5左右,我选择0.7),当每次更新后,递归回去检查i节点的左右儿子分别乘以平衡因子,是否大于另一个儿子,如果大了,代表这棵树有退化的倾向,赶紧拍平重建(就是把树压成链表,重新建树)。

  大概就是这样,手机打的好难受,直接上代码好了。

  (其实我一开始做平衡树时觉得可以用线段树模拟的emmm,就不说了)

  代码可以配合洛谷的模板题食用

#include <cstdio>
#include <iostream>
using namespace std;
const int INF=;
const int MAX_N=;
const double alpha=0.75; int n;
inline int read(){
register int ch=getchar(),x=,f=;
while (ch<''||ch>''){
if (ch=='-') f=-;
ch=getchar();
} while (ch>=''&&ch<=''){
x=x*+ch-'';
ch=getchar();
} return x*f;
}
struct Tree{
int fa;
int size;
int value;
int son[];
}tree[MAX_N];
int cnt=;
int root=;
int node[MAX_N];
int sum; bool balance(int x){ //判断是否平衡
return (double)tree[x].size*alpha>=(double)tree[tree[x].son[]].size&&(double)tree[x].size*alpha>=(double)tree[tree[x].son[]].size;
}
int build(int l,int r){ //重新递归建树
if (l>r) return ;
int mid=(l+r)>>;
tree[tree[node[mid]].son[]=build(l,mid-)].fa=node[mid],tree[tree[node[mid]].son[]=build(mid+,r)].fa=node[mid];
tree[node[mid]].size=tree[tree[node[mid]].son[]].size+tree[tree[node[mid]].son[]].size+;
return node[mid];
}
void recycle(int x){ //把树压成数列
if (tree[x].son[]) recycle(tree[x].son[]);
node[++sum]=x;
if (tree[x].son[]) recycle(tree[x].son[]);
}
void rebuild(int x){
sum=;
recycle(x);
int fa=tree[x].fa,son=(tree[tree[x].fa].son[]==x),now=build(,sum);
tree[tree[fa].son[son]=now].fa=fa;
if (x==root) root=now;
}
void insert(int x){
int i=root,now=++cnt; //新节点序号
tree[now].size=,tree[now].value=x;
while (true){
tree[i].size++;
bool son=(x>=tree[i].value);
if (tree[i].son[son]) i=tree[i].son[son];
else{
tree[tree[i].son[son]=now].fa=i;
break;
}
}
int flag=;
for (int j=now;j;j=tree[j].fa) //logn找不平衡的节点
if (!balance(j)) flag=j;
if (flag) rebuild(flag); //重建树
} int get_num(int x){
int i=root;
while (true){
if(tree[i].value==x) return i;
else i=tree[i].son[tree[i].value<x];
}
}
void erase(int x){ //删除
if (tree[x].son[]&&tree[x].son[]){
int now=tree[x].son[];
while (tree[now].son[]) now=tree[now].son[];
tree[x].value=tree[now].value;
x=now;
}
int son=(tree[x].son[])?tree[x].son[]:tree[x].son[];
int k=(tree[tree[x].fa].son[]==x);
tree[tree[tree[x].fa].son[k]=son].fa=tree[x].fa;
for (int i=tree[x].fa;i;i=tree[i].fa)
tree[i].size--;
if (x==root)
root=son;
}
int get_rank(int x){
int i=root,ans=;
while (i)
if(tree[i].value<x) ans+=tree[tree[i].son[]].size+,i=tree[i].son[];
else i=tree[i].son[];
return ans;
}
int get_kth(int x){
int i=root;
while (true)
if (tree[tree[i].son[]].size==x-) return i;
else if (tree[tree[i].son[]].size>=x) i=tree[i].son[];
else x-=tree[tree[i].son[]].size+,i=tree[i].son[];
return i;
}
int get_front(int x){
int i=root,ans=-INF;
while(i)
if(tree[i].value<x) ans=max(ans,tree[i].value),i=tree[i].son[];
else i=tree[i].son[];
return ans;
}
int get_behind(int x){
int i=root,ans=INF;
while(i)
if(tree[i].value>x) ans=min(ans,tree[i].value),i=tree[i].son[];
else i=tree[i].son[];
return ans;
}
int main(){
// freopen("test1.in","r",stdin);
tree[].value=-INF,tree[].size=,tree[].son[]=;
tree[].value=INF,tree[].size=,tree[].fa=;
n=read();
for(int i=,op,x;i<=n;i++){
op=read(),x=read();
if(op==) insert(x);
if(op==) erase(get_num(x));
if(op==) printf("%d\n",get_rank(x));
if(op==) printf("%d\n",tree[get_kth(x+)].value);
if(op==) printf("%d\n",get_front(x));
if(op==) printf("%d\n",get_behind(x));
}
}

其他例题

大体上按难度排序?我太弱了也搞不懂。

一.[HNOI2002]营业额统计 (2019.4.10更新)

洛谷oj P2234

  据说有各种神犇用许多神奇的解法A掉了……但是我这种蒟蒻就先用平衡树练手了。

  min{|该天以前某一天的营业额-该天营业额|},即不大于a[i]的最大值和不小于a[i]的最小值,就是寻找a[i]的前驱和后驱,分别减去a[i]取绝对值,再全部加起来,复杂度应该是O(nlogn)。

  代码:

#include <cstdio>
#include <iostream>
using namespace std;
const int INF=;
const int MAX_N=;
const double alpha=0.75; int n;
inline int read(){
register int ch=getchar(),x=,f=;
while (ch<''||ch>''){
if (ch=='-') f=-;
ch=getchar();
} while (ch>=''&&ch<=''){
x=x*+ch-'';
ch=getchar();
} return x*f;
}
struct Tree{
int fa;
int size;
int value;
int son[];
}tree[MAX_N];
int cnt=;
int root=;
int node[MAX_N];
int sum; bool balance(register int x){ //判断是否平衡
return (double)tree[x].size*alpha>=(double)tree[tree[x].son[]].size&&(double)tree[x].size*alpha>=(double)tree[tree[x].son[]].size;
}
inline int build(register int l,register int r){ //重新递归建树
if (l>r) return ;
int mid=(l+r)>>;
tree[tree[node[mid]].son[]=build(l,mid-)].fa=node[mid],tree[tree[node[mid]].son[]=build(mid+,r)].fa=node[mid];
tree[node[mid]].size=tree[tree[node[mid]].son[]].size+tree[tree[node[mid]].son[]].size+;
return node[mid];
}
void recycle(register int x){ //把树压成数列
if (tree[x].son[]) recycle(tree[x].son[]);
node[++sum]=x;
if (tree[x].son[]) recycle(tree[x].son[]);
}
void rebuild(register int x){
sum=;
recycle(x);
int fa=tree[x].fa,son=(tree[tree[x].fa].son[]==x),now=build(,sum);
tree[tree[fa].son[son]=now].fa=fa;
if (x==root) root=now;
}
void insert(register int x){
int i=root,now=++cnt; //新节点序号
tree[now].size=,tree[now].value=x;
while (true){
tree[i].size++;
bool son=(x>=tree[i].value);
if (tree[i].son[son]) i=tree[i].son[son];
else{
tree[tree[i].son[son]=now].fa=i;
break;
}
}
int flag=;
for (int j=now;j;j=tree[j].fa) //logn找不平衡的节点
if (!balance(j)) flag=j;
if (flag) rebuild(flag); //重建树
} inline int get_front(register int x){
register int i=root,ans=-INF;
while(i)
if(tree[i].value<x) ans=max(ans,tree[i].value),i=tree[i].son[];
else i=tree[i].son[];
return ans;
}
inline int get_behind(register int x){
register int i=root,ans=INF;
while(i)
if(tree[i].value>x) ans=min(ans,tree[i].value),i=tree[i].son[];
else i=tree[i].son[];
return ans;
}
bool flag[+];
int result;
int main(){
// freopen("test1.in","r",stdin);
tree[].value=-INF,tree[].size=,tree[].son[]=;
tree[].value=INF,tree[].size=,tree[].fa=;
n=read();
int kkk=read();
result+=kkk;
insert(kkk);
flag[kkk]=true;
for (int i=,a,min,max;i<=n;i++){
a=read();
if (flag[a]) continue;
flag[a]=true;
insert(a);
min=get_front(a);
max=get_behind(a);
if (min==-INF){
result+=(max-a);
continue;
}
else if (max==INF){
result+=(a-min);
continue;
}
result+=(a-min>max-a?max-a:a-min);
}
printf("%d",result);
return ;
}

[学习自百度百科和其他网络资料]

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

  1. 简析平衡树(一)——替罪羊树 Scapegoat Tree

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

  2. [TYVJ1728/BZOJ3224]普通平衡树-替罪羊树

    Problem 普通平衡树 Solution 本题是裸的二叉平衡树.有很多种方法可以实现.这里打的是替罪羊树模板. 此题极其恶心. 前驱后继模块需要利用到rank模块来换一种思路求. 很多细节的地方容 ...

  3. Luogu 3369 / BZOJ 3224 - 普通平衡树 - [替罪羊树]

    题目链接: https://www.lydsy.com/JudgeOnline/problem.php?id=3224 https://www.luogu.org/problemnew/show/P3 ...

  4. bzoj2827: 千山鸟飞绝 平衡树 替罪羊树 蜜汁标记

    这道题首先可以看出坐标没有什么意义离散掉就好了. 然后你就会发现你要每次都更改坐标,而一旦更改受影响的是坐标里的所有数,要是一个一个的改,会不可描述. 所以换个视角,我们要找的是某只鸟所到每个坐标时遇 ...

  5. bzoj 3224: Tyvj 1728 普通平衡树 替罪羊树

    题目链接 您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:1. 插入x数2. 删除x数(若有多个相同的数,因只删除一个)3. 查询x数的排名(若有多个相同的数,因输出最小的 ...

  6. [luogu3369]普通平衡树(替罪羊树模板)

    解题关键:由于需要根据平衡进行重建,所以不能进行去重,否则无法保证平衡性. #include<cstdio> #include<cstring> #include<alg ...

  7. Bzoj3224 / Tyvj 1728 普通替罪羊树

    Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 12015  Solved: 5136 Description 您需要写一种数据结构(可参考题目标题), ...

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

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

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

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

随机推荐

  1. Laravel 上传文件处理

    文件上传 获取上传的文件 可以使用 Illuminate\Http\Request 实例提供的 file 方法或者动态属性来访问上传文件, file 方法返回 Illuminate\Http\Uplo ...

  2. Markdown 手册

    前言(可以不看) 最开始只是想写一篇博文,准备使用markdown,感觉很流行(github.简书……很多都支持),而且渲染出来很好看,一直很想学,没有合适的机会,结果拖到了现在.比起什么python ...

  3. 常见的C语言内存错误及对策(转)

    http://see.xidian.edu.cn/cpp/html/483.html 一.指针没有指向一块合法的内存 定义了指针变量,但是没有为指针分配内存,即指针没有指向一块合法的内存.浅显的例子就 ...

  4. HDU 5117 Fluorescent (数学+状压DP)

    题意:有 n 个灯,初始状态都是关闭,有m个开关,每个开关都控制若干个.问在m个开关按下与否的2^m的情况中,求每种情况下亮灯数量的立方和. 析:首先,如果直接做的话,时间复杂度无法接受,所以要对其进 ...

  5. 疯狂JAVA讲义---第十五章:输入输出(上)流的处理和文件

    在Java中,把这些不同类型的输入.输出抽象为流(Stream),而其中输入或输出的数据称为数据流(Data Stream),用统一的接口来表示,从而使程序设计简单明了. 首先我要声明下:所谓的输入输 ...

  6. 浅析互联网系统和传统企业IT系统的异同

    前不久,一则中行宕机的消息引起了网上IT人士的热议.其中对于大型机或者RISC系统的稳定性可靠性的质疑更是热议中的主流声音,很多人拿现在互联网系统做对比,认为大型机所谓的几个9都是吹出来的云云.在这里 ...

  7. linux 搭建php网站许愿墙

    网站素材在:https://i.cnblogs.com/Files.aspx 首先需要搭建本地yum源,详情参考: http://www.cnblogs.com/jw35/p/5967677.html ...

  8. [label][JavaScript] 自动填充内容的JavaScript 库

    一个帮助你针对不同标签自动填入内容的轻量级javascript类库 - fixiejs http://www.gbtags.com/technology/javascript/20120802-fix ...

  9. 深入CSS属性(九):z-index

    如果你不是一名csser新手,想必你对z-index的用法应该有个大致的了解了吧,z-index可以控制定位元素在垂直于显示屏方向(Z 轴)上的堆叠顺序,本文不去讲述基本的API如何使用,而是去更深入 ...

  10. JAVA 字符串编码转换

    /** * 字符串编码转换的实现方法 * @param str 待转换编码的字符串 * @param newCharset 目标编码 * @return * @throws UnsupportedEn ...