平衡树 替罪羊树(Scapegoat Tree)
替罪羊树(Scapegoat Tree)
题目描述
您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
- 插入xx数
- 删除xx数(若有多个相同的数,因只删除一个)
- 查询xx数的排名(排名定义为比当前数小的数的个数+1+1。若有多个相同的数,因输出最小的排名)
- 查询排名为xx的数
- 求xx的前驱(前驱定义为小于xx,且最大的数)
- 求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),当然也可以叫它二叉搜索树,或者二叉排序树(反正都一个意思都是二叉树),它的定义如下:
或者是一棵空树,或者是具有下列性质的二叉树:
差不多就像下面这幅图一样:
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更新)
据说有各种神犇用许多神奇的解法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)的更多相关文章
- 简析平衡树(一)——替罪羊树 Scapegoat Tree
前言 平衡树在我的心目中,一直都是一个很高深莫测的数据结构.不过,由于最近做的题目的题解中经常出现"平衡树"这三个字,我决定从最简单的替罪羊树开始,好好学习平衡树. 简介 替罪羊树 ...
- [TYVJ1728/BZOJ3224]普通平衡树-替罪羊树
Problem 普通平衡树 Solution 本题是裸的二叉平衡树.有很多种方法可以实现.这里打的是替罪羊树模板. 此题极其恶心. 前驱后继模块需要利用到rank模块来换一种思路求. 很多细节的地方容 ...
- Luogu 3369 / BZOJ 3224 - 普通平衡树 - [替罪羊树]
题目链接: https://www.lydsy.com/JudgeOnline/problem.php?id=3224 https://www.luogu.org/problemnew/show/P3 ...
- bzoj2827: 千山鸟飞绝 平衡树 替罪羊树 蜜汁标记
这道题首先可以看出坐标没有什么意义离散掉就好了. 然后你就会发现你要每次都更改坐标,而一旦更改受影响的是坐标里的所有数,要是一个一个的改,会不可描述. 所以换个视角,我们要找的是某只鸟所到每个坐标时遇 ...
- bzoj 3224: Tyvj 1728 普通平衡树 替罪羊树
题目链接 您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:1. 插入x数2. 删除x数(若有多个相同的数,因只删除一个)3. 查询x数的排名(若有多个相同的数,因输出最小的 ...
- [luogu3369]普通平衡树(替罪羊树模板)
解题关键:由于需要根据平衡进行重建,所以不能进行去重,否则无法保证平衡性. #include<cstdio> #include<cstring> #include<alg ...
- Bzoj3224 / Tyvj 1728 普通替罪羊树
Time Limit: 10 Sec Memory Limit: 128 MBSubmit: 12015 Solved: 5136 Description 您需要写一种数据结构(可参考题目标题), ...
- [模板] 平衡树: Splay, 非旋Treap, 替罪羊树
简介 二叉搜索树, 可以维护一个集合/序列, 同时维护节点的 \(size\), 因此可以支持 insert(v), delete(v), kth(p,k), rank(v)等操作. 另外, prev ...
- 在平衡树的海洋中畅游(二)——Scapegoat Tree
在平衡树的广阔天地中,以Treap,Splay等为代表的通过旋转来维护平衡的文艺平衡树占了觉大部分. 然而,今天我们要讲的Scapegoat Tree(替罪羊树)就是一个特立独行的平衡树,它通过暴力重 ...
随机推荐
- Laravel 上传文件处理
文件上传 获取上传的文件 可以使用 Illuminate\Http\Request 实例提供的 file 方法或者动态属性来访问上传文件, file 方法返回 Illuminate\Http\Uplo ...
- Markdown 手册
前言(可以不看) 最开始只是想写一篇博文,准备使用markdown,感觉很流行(github.简书……很多都支持),而且渲染出来很好看,一直很想学,没有合适的机会,结果拖到了现在.比起什么python ...
- 常见的C语言内存错误及对策(转)
http://see.xidian.edu.cn/cpp/html/483.html 一.指针没有指向一块合法的内存 定义了指针变量,但是没有为指针分配内存,即指针没有指向一块合法的内存.浅显的例子就 ...
- HDU 5117 Fluorescent (数学+状压DP)
题意:有 n 个灯,初始状态都是关闭,有m个开关,每个开关都控制若干个.问在m个开关按下与否的2^m的情况中,求每种情况下亮灯数量的立方和. 析:首先,如果直接做的话,时间复杂度无法接受,所以要对其进 ...
- 疯狂JAVA讲义---第十五章:输入输出(上)流的处理和文件
在Java中,把这些不同类型的输入.输出抽象为流(Stream),而其中输入或输出的数据称为数据流(Data Stream),用统一的接口来表示,从而使程序设计简单明了. 首先我要声明下:所谓的输入输 ...
- 浅析互联网系统和传统企业IT系统的异同
前不久,一则中行宕机的消息引起了网上IT人士的热议.其中对于大型机或者RISC系统的稳定性可靠性的质疑更是热议中的主流声音,很多人拿现在互联网系统做对比,认为大型机所谓的几个9都是吹出来的云云.在这里 ...
- linux 搭建php网站许愿墙
网站素材在:https://i.cnblogs.com/Files.aspx 首先需要搭建本地yum源,详情参考: http://www.cnblogs.com/jw35/p/5967677.html ...
- [label][JavaScript] 自动填充内容的JavaScript 库
一个帮助你针对不同标签自动填入内容的轻量级javascript类库 - fixiejs http://www.gbtags.com/technology/javascript/20120802-fix ...
- 深入CSS属性(九):z-index
如果你不是一名csser新手,想必你对z-index的用法应该有个大致的了解了吧,z-index可以控制定位元素在垂直于显示屏方向(Z 轴)上的堆叠顺序,本文不去讲述基本的API如何使用,而是去更深入 ...
- JAVA 字符串编码转换
/** * 字符串编码转换的实现方法 * @param str 待转换编码的字符串 * @param newCharset 目标编码 * @return * @throws UnsupportedEn ...