普通平衡树模板以及文艺平衡树模板链接.

简介

平衡二叉树(Balanced Binary Tree)具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树(摘自百度百科)。

splay又名Splay Balanced Tree(SBT),通过双旋来维持它平衡树的性质.

同时有类似的结构Spaly 我也不知道是不是真的有 , 只用单选来维护平衡树.

struct node{
int fa;//记录节点父亲
int ch[2];//ch[0]表示左儿子,ch[1]表示右儿子
int val;//记录节点权值
int size;//记录节点子数大小(包括该节点)
int cnt;//记录同样权值的元素个数
int mark;//记录反转区间标记(普通平衡树不用)
}t[N];

另外补充说明一下size在记录子树大小的时候指的是以node为根的整颗子树的元素个数,而不是节点个数(有相同权值的时候都要统计进来).

splay具有这样的性质:

  1. 一个节点的权值总比它的左儿子大,比它的右儿子小.
  2. splay树的中序遍历结果就是该序列从小到大排列.

下面先介绍一下旋转操作.

旋转首先需要查找一个节点属于左节点还是右节点.

bool get(int x){
return t[t[x].fa].ch[1] == x;//是右儿子返回1,左儿子返回0
}

并且在将一个节点向上旋的过程中因为节点的关系发生了变化,所以需要重新统计.

void up(int x){
t[x].size=t[t[x].ch[0]].size+t[t[x].ch[1]].size+t[x].cnt;
}

假设现在要将x右旋到fa的位置(向哪个方向旋就叫什么旋),那么步骤如下:

先找出x属于哪边儿子(左儿子还是右儿子):d1(d1为0表示x是左儿子,为1表示是右儿子).图中d1==0(x是左儿子).然后断开x与 t[x].ch[d1^1] 的连边,并将 t[x].ch[d1^1] 连到fa上代替x的位置.

然后断开father与grandfather的连边,将x接上去代替father的位置,并将father以及它整颗子树向下拉.

最后再把father与x连边,一次旋转就完成了.

void rotate(int x){
int fa = t[x].fa , gfa = t[fa].fa;
int d1 = get(x) , d2 = get(fa);
t[fa].ch[d1] = t[x].ch[d1^1] ; t[t[x].ch[d1^1]].fa = fa;
t[gfa].ch[d2] = x; t[x].fa = gfa;
t[fa].fa = x; t[x].ch[d1^1] = fa;
up(fa); up(x);//up是收集节点子树的个数
}

这样旋转后并没有改变它二叉平衡树的性质.并且双旋操作可以减小平衡树的期望深度. (至于为什么可以自己出一组稍微大一点的数据模拟一下)

双旋操作指的是当要旋转的节点与它父亲在同一边时(它和父亲都是左儿子或右儿子),先旋转父亲,再旋转它自己.

play操作其实就是模拟的一个节点向上转的过程,下面直接看注释:

void splay(int x,int goal){//goal是将x旋转到goal的下面
while(t[x].fa != goal){
int fa = t[x].fa , gfa = t[fa].fa;
int d1 = get(x) , d2 = get(fa);
if(gfa != goal){
if(d1 == d2) rotate(fa);//若在同边,则先转father(双旋)
else rotate(x);//否则直接将x向上旋
}
rotate(x);//再向上旋一次
}
if(goal == 0) root = x;//用goal==0来表示将x转到根节点
}

下面看一下splay过程的图解(splay(x,goal)):

(原图)

(x与father同侧,先转father)

(再转x)

(最后将x旋转到goal的下面).

通过上面这几个函数,我们已经可以维护splay它平衡树的性质了,然后splay还有一些操作:

  1. 插入一个数字.
  2. 删除一个数字.
  3. 查询一个数字的排名.
  4. 查找一个数字的前驱/后继.
  5. 查询第k小的数字是多少.
  6. 查询最值.


首先我们来看如何插入一个数字.

插入节点时是按照新插入的节点权值来遍历splay找到它应该插入的位置的.所以就在遍历splay时记录一下父亲,直到找到应该插入的位置就加新节点.

void insert(int val){
int node = root , fa = 0;
while(node && t[node].val != val)
fa = node , node = t[node].ch[t[node].val<val];
if(node) t[node].cnt++;//如果已经存在该权值的节点,则直接给该节点所含相同数字个数++
else{//否则新开一个编号存节点
node = ++cnt; if(fa) t[fa].ch[t[fa].val<val] = node;
t[node].fa = fa;
t[node].val = val;
t[node].cnt = 1;
t[node].size = 1;
}
splay(node , 0);//将新节点旋到根来维护splay的子树
}

最后将新插入的节点旋到根,新插入节点后的splay树就被维护好了.


查询第k小的数字

我们已经知道了splay的二叉平衡树的性质,并且通过splay操作维持了它平衡树的性质,那么在查询第k小的数字时,就可以直接比较k与节点size的大小来确定第k小的数字在哪个位置了.我们用node来表示当前遍历到的节点(最开始从root出发).

  • 如果k比node左子树的size值要小的话,那么第k小一定在node的左子树中.
  • 如果k比node左子树和node节点所含数字个数还要多,那么一定在右子树中.
  • 如果这两个情况都不满足,则node就是第k小.
int kth(int k){
int node = root;
while(1){
int son = t[node].ch[0];
if(k <= t[son].size) node = son;
else if(k > t[son].size+t[node].cnt){
k -= t[son].size + t[node].cnt;
node = t[node].ch[1];
}
else return t[node].val;
}
}


查询一个数的排名

要查找一个数的排名,首先要找到它在splay树中的位置.同样也是通过权值来遍历.

int find(int val){
int node = root;
while(t[node].val!=val && t[node].ch[t[node].val<val])
node = t[node].ch[t[node].val<val];
return node;
}

这样找出来的编号就是权值为val的节点.若不存在这样的节点,则会找到叶子节点(此时权值不一定最接近查询的值,但是可以通过这样来找树中的最值).




找到要查的数字后,直接将它旋转到根,此时它左子树的size+1就是它的排名.

int get_rank(int val){
splay(find(val) , 0);
return t[t[root].ch[0]].size+1;
}


查找一个数的前驱/后继

为了方便操作,可以先把要查找的值先旋到根.

此时如果要查询前驱,前驱一定就是根节点或是在它的左子树中最大的值.那么先比较根节点的权值与要查询的值,如果要查询前驱并且根节点的权值已经比要找前驱的权值要小了,那么根节点就是要查找的前驱.

为什么一定是这样的呢?因为find找到一个结点要么找到的是该节点,要么就是与要找的权值最接近的节点.所以根节点的权值与查找的权值最接近.而前驱就是比它权值要小的最大的数,所以根节点就是前驱了.

如果根节点不是前驱,那么前驱就是它左子树中的最大值(也就是左子树最右边的节点).

int get_pre(int val,int kind){//前驱后继查询写在了同一个函数里,kind==0表示查找前驱,kind==0表示查找后继
splay(find(val) , 0); int node = root;
if(t[node].val<val && kind == 0) return node;
if(t[node].val>val && kind == 1) return node;//根节点就是前驱/后继的的情况
node = t[node].ch[kind];
while(t[node].ch[kind^1])
node = t[node].ch[kind^1];//否则找到根节点子树中的最值
return node;
}


删除一个数

删除一个数,也是先要确定这个节点的位置.但是删除一个节点不能直接将要删除的节点旋转到根.因为如果旋转到根节点之后它有可能还有左右子树.

所以我们可以先找到它的前驱后继,然后将前驱旋到根,后继旋到前驱的下面.此时要删除的点就是后继的左儿子.

因为前驱是第一个比它小的数字,所以它在前驱的右边,后继是第一个比它大的数字,所以他在后继的左边,后继旋到了前驱的下面,那么要删除的节点就一定在前驱后继的中间,也就是后继的左儿子.

然后找到它的位置进行删除.

void delet(int val){
int last = get_pre(val,0);
int next = get_pre(val,1);
splay(last , 0); splay(next , last);
if(t[t[next].ch[0]].cnt > 1){
t[t[next].ch[0]].cnt--;
splay(t[next].ch[0],0);//同样将未删完的节点转到根重新统计子树大小
}
else t[next].ch[0] = 0;//如果能直接删除,则直接去掉这条连边
}


查询最值

查询最值也是通过find函数会找到与一个数最接近的节点的特性,直接find(inf)或者是find(-inf)来找与正无穷最接近的值(最大值)或与负无穷最接近的值(最小值).


到这里,splay的基本操作就讲完了.下面是模板代码.

普通平衡树

#include<bits/stdc++.h>
#define b out(root),cout << endl;
using namespace std;
const int N=100000+5;
const int inf=2147483647; int n;
int cnt = 0;
int root = 0; struct splay{
int ch[2], size, cnt, val, fa;
}t[N]; int gi(){
int ans = 0 , f = 1; char i = getchar();
while(i<'0'||i>'9'){if(i=='-')f=-1;i=getchar();}
while(i>='0'&&i<='9'){ans=ans*10+i-'0';i=getchar();}
return ans * f;
} void out(int x){
if(t[x].ch[0]) out(t[x].ch[0]);
printf("%d ",t[x].val);
if(t[x].ch[1]) out(t[x].ch[1]);
} int get(int x){
return t[t[x].fa].ch[1] == x;
} void up(int x){
t[x].size=t[t[x].ch[0]].size+t[t[x].ch[1]].size+t[x].cnt;
} void rotate(int x){
int fa = t[x].fa , gfa = t[fa].fa;
int d1 = get(x) , d2 = get(fa);
t[fa].ch[d1]=t[x].ch[d1^1] , t[t[x].ch[d1^1]].fa=fa;
t[gfa].ch[d2]=x , t[x].fa=gfa;
t[fa].fa=x , t[x].ch[d1^1]=fa;
up(fa); up(x);
} void splay(int x,int goal){
while(t[x].fa != goal){
int fa = t[x].fa, gfa = t[fa].fa;
int d1 = get(x), d2 = get(fa);
if(gfa != goal){
if(d1 == d2) rotate(fa);
else rotate(x);
}
rotate(x);
}
if(goal == 0) root = x;
} int find(int val){
int node = root;
while(t[node].val != val && t[node].ch[t[node].val<val])
node = t[node].ch[t[node].val<val];
return node;
} void insert(int val){
int node = root, fa = 0;
while(t[node].val != val && node)
fa = node, node = t[node].ch[t[node].val<val];
if(node) t[node].cnt++;
else{
node = ++cnt;
if(fa) t[fa].ch[t[fa].val<val] = node;
t[node].size = t[node].cnt = 1;
t[node].fa = fa; t[node].val = val;
}
splay(node , 0);
} int pre(int val,int kind){
splay(find(val) , 0); int node = root;
if(t[node].val < val && kind == 0) return node;
if(t[node].val > val && kind == 1) return node;
node = t[node].ch[kind];
while(t[node].ch[kind^1])
node = t[node].ch[kind^1];
return node;
} void delet(int val){
int last = pre(val,0), next = pre(val,1);
splay(last , 0); splay(next , last);
if(t[t[next].ch[0]].cnt > 1){
t[t[next].ch[0]].cnt--;
splay(t[next].ch[0] , 0);
}
else t[next].ch[0] = 0;
} int kth(int k){
int node = root;
if(t[node].size < k) return inf;
while(1){
int son = t[node].ch[0];
if(k <= t[son].size) node = son;
else if(k > t[son].size+t[node].cnt){
k -= t[son].size+t[node].cnt;
node = t[node].ch[1];
}
else return t[node].val;
}
} int get_rank(int val){
splay(find(val) , 0);
return t[t[root].ch[0]].size;
} int main(){
insert(-inf); insert(inf);
int flag, x; n = gi();
for(int i=1;i<=n;i++){
flag = gi(); x = gi();
if(flag == 1) insert(x);
if(flag == 2) delet(x);
if(flag == 3) printf("%d\n",get_rank(x));
if(flag == 4) printf("%d\n",kth(x+1));
if(flag == 5) printf("%d\n",t[pre(x,0)].val);
if(flag == 6) printf("%d\n",t[pre(x,1)].val);
}
return 0;
}

当然,这还不够,splay还有一个强大的功能:翻转区间

要找到一段区间,可以利用删除数字的思想.先找到区间左端点的前驱旋转到根,再找到区间右端点的后继旋转到前驱下面,此时要找的区间就能确定就是后继的左子树.然后再给节点打上标记,用线段树的思想不断处理标记,最后再查询的时候再将标记下放,就可以维护出翻转后的序列.

文艺平衡树

#include<bits/stdc++.h>
using namespace std;
const int N=100000+5;
const int inf=2147483647; int n, m;
int cnt = 0;
int root = 0; struct node{
int ch[2], fa, size, mark, val;
}t[N]; bool get(int x){
return t[t[x].fa].ch[1] == x;
} void up(int x){
t[x].size = t[t[x].ch[0]].size + t[t[x].ch[1]].size + 1;
} void rotate(int x){
int fa = t[x].fa , gfa = t[fa].fa , d1 = get(x) , d2 = get(fa);
t[fa].ch[d1] = t[x].ch[d1^1]; t[t[x].ch[d1^1]].fa = fa;
t[gfa].ch[d2] = x; t[x].fa = gfa;
t[fa].fa = x; t[x].ch[d1^1] = fa;
up(fa); up(x);
} void splay(int x,int goal){
while(t[x].fa != goal){
int fa = t[x].fa , gfa = t[fa].fa;
int d1 = get(x) , d2 = get(fa);
if(gfa != goal){
if(d1 == d2) rotate(fa);
else rotate(x);
}
rotate(x);
}
if(goal == 0) root = x;
//printf("root = %d\n",root);
} void insert(int val){
int node = root , fa = 0;
while(node && t[node].val != val)
fa = node , node = t[node].ch[t[node].val<val];
node = ++cnt;
if(fa) t[fa].ch[t[fa].val<val] = node;
t[node].fa = fa;
t[node].val = val;
t[node].size = 1;
splay(node , 0);
} void pushdown(int x){
t[t[x].ch[0]].mark ^= 1;
t[t[x].ch[1]].mark ^= 1;
t[x].mark = 0;
swap(t[x].ch[0] , t[x].ch[1]);
} int kth(int k){
int node = root;
while(1){
if(t[node].mark) pushdown(node);
int son = t[node].ch[0];
if(k<=t[son].size) node = son;
else if(k>t[son].size+1){
k -= t[son].size+1;
node = t[node].ch[1];
}
else return node;
}
} void work(int l,int r){
int left = kth(l) , right = kth(r);
splay(left , 0) ; splay(right , left);
t[t[t[root].ch[1]].ch[0]].mark ^= 1;
} void output(int x){
if(t[x].mark) pushdown(x);
if(t[x].ch[0]) output(t[x].ch[0]);
if(t[x].val>=1 && t[x].val<=n) printf("%d ",t[x].val);
if(t[x].ch[1]) output(t[x].ch[1]);
} int main(){
insert(inf); insert(-inf);
int x, y; cin >> n >> m;
for(int i=1;i<=n;i++) insert(i);
for(int i=1;i<=m;i++){
scanf("%d%d",&x,&y);
work(x , y+2);
}
output(root); cout << endl;
return 0;
}

明白了这几个模板之后,就可以做点简单的题目练练手了.

LIST

  1. [HNOI2002]营业额统计
  2. [NOI2004]郁闷的出纳员
  3. [JSOI2008]最大数
  4. [NOI2003]文本编辑器
  5. [ZJOI2006]书架
  6. [HNOI2004]宠物收养场
  7. [HNOI2012]永无乡
  8. [NOI2005]维护数列

题解

T1:

动态查询前驱并统计答案,没什么好讲的吧.


T2:

对于修改所有人的工资,可以直接用变量保存所有人被修改的工资而不用一个个修改.然后在查询的时候直接找到第一个比劝退标准低的人,删除的时候直接把它以及它左边的全部删掉.也就是在删除的过程中找到它以及它子树的位置旋到根节点的右儿子的左儿子,然后直接删除它的父指针.


T3:

插入的时候直接插入到树的最右边,这样维护的一颗splay的中序遍历结果就是这个序列了.然后在节点维护一个最大值,查找的时候就先找到它前面一个元素的排名旋转到根,那么根节点的右儿子的最大值就是答案了.


T4:

按照题意模拟,可以在插入一个序列的时候先将这个序列处理成一棵树然后再合并.


T5:

可以考虑给每个元素定义一个优先值来维护平衡树的性质(反正当时我做这道题的时候老是搞不清,就这么写了).用一个数组记录一本书的编号映射到树中的优先值.

  • 对于要放到书架顶端的书,先将它删除,然后再给它赋一个最小值插入树中.
  • 对于要放到书架底端的书同理.
  • 对于要与前驱后继交换的书,先交换编号映射的优先值,然后再分别删除,插入这两个点.
  • 其他直接模板操作解决.


T6:

因为没有领养者的时候来领养者,或是没有宠物的时候来宠物,都会找到目前树中与该值最接近的一个(前驱后继中取min),所以考虑用一个计数器统计当前领养者/宠物数,来表示目前状态的树为宠物树/领养者树,然后再对这些情况分类讨论一下就可以了.


T7:

链接

<\br>

T8:

按照题意模拟...注意细节,具体代码实现可以戳这里

Splay模板讲解及一些题目的更多相关文章

  1. bzoj 1588 splay模板题

    用晚自习学了一下splay模板,没想象中那么难,主要是左旋和右旋可以简化到一个函数里边,减少代码长度... #include<iostream> #include<cstdio> ...

  2. COJ 1002 WZJ的数据结构(二)(splay模板)

    我的LCC,LCT,Splay格式终于统一起来了... 另外..这个形式的Splay是标准的Splay(怎么鉴别呢?看Splay函数是否只传了一个变量node就行),刘汝佳小白书的Splay写的真是不 ...

  3. Splay 模板

    Splay 模板 struct SplayTree{ const static int maxn = 1e5 + 15; int ch[maxn][2] , key[maxn] , s[maxn] , ...

  4. [luogu3369/bzoj3224]普通平衡树(splay模板、平衡树初探)

    解题关键:splay模板题整理. 如何不加入极大极小值?(待思考) #include<cstdio> #include<cstring> #include<algorit ...

  5. BZOJ1588 [HNOI2002]营业额统计 splay模板

    1588: [HNOI2002]营业额统计 Time Limit: 5 Sec  Memory Limit: 162 MB Submit: 16189  Solved: 6482 [Submit][S ...

  6. 文艺平衡树(splay模板)

    题干:splay模板,要求维护区间反转. splay是一种码量小于treap,但支持排名,前驱后继等treap可求的东西,也支持区间反转的平衡树. 但是有两个坏处: 1.splay常数远远大于trea ...

  7. [洛谷P3391] 文艺平衡树 (Splay模板)

    初识splay 学splay有一段时间了,一直没写...... 本题是splay模板题,维护一个1~n的序列,支持区间翻转(比如1 2 3 4 5 6变成1 2 3 6 5 4),最后输出结果序列. ...

  8. POJ 3481 splay模板

    最后撸一发splay. 之前用treap撸的,现在splay也找到感觉了,果然不同凡响,两者之间差别与精妙之处各有其精髓! 真心赞一个! POJ平衡树的题目还是比较少,只能挑之前做过的捏一捏.但是收获 ...

  9. bzoj3224 普通平衡树 splay模板

    题目传送门 题目大意:完成一颗splay树. 思路:模板题,学着还是很有意思的. 学习splay树:蒟蒻yyb 该题模板:汪立超 #include<bits/stdc++.h> #defi ...

随机推荐

  1. flask_admin 笔记五 内置模板设置

    内建模板 Flask-Admin是使用jinja2模板引擎 1)扩展内建的模板 不要完全覆盖内置的模板,最好是扩展它们. 这将使您更容易升级到新的Flask-Admin版本. 在内部,Flask-Ad ...

  2. OPENSTACK重装系统失败导致虚拟机状态为error

    重装系统失败导致虚拟机状态为error DASHBOARD查看虚拟机状态: 查看日志: 磁盘不足导致下载新镜像失败. Virsh list -all 无法发现虚拟机,底层盘消失(因为重装系统时nova ...

  3. c语言数字图像处理(一):bmp图片格式及灰度图片转换

    本篇文章首先介绍了bmp图片格式,主要参考wiki上的内容,包括bmp文件的存储方式,对于一些常见的bmp文件格式都给了例子,并且对8位 16位RGB555 16位RGB565格式的bmp文件进行了简 ...

  4. FINAUNCE金融业增速反弹信贷投放创新高叠加股市回暖

    FINAUNCE金融业增速反弹信贷投放创新高叠加股市回暖,金融业增加值增速回暖,不过难以回到2015年的巅峰. 国家统计局4月18日发布的数据显示,今年一季度,国内生产总值21.34万亿元,按可比价格 ...

  5. Go实现Pow工作量证明

    之前使用python编写了一段代码实现了工作量证明机制,近期由于参与以太坊智能合约开发钱包的工作接触到golang语言,所以借此以go来实现Pow(Proof of work). 实现代码如下: // ...

  6. PAT甲题题解-1110. Complete Binary Tree (25)-(判断是否为完全二叉树)

    题意:判断一个节点为n的二叉树是否为完全二叉树.Yes输出完全二叉树的最后一个节点,No输出根节点. 建树,然后分别将该树与节点树为n的二叉树相比较,统计对应的节点个数,如果为n,则为完全二叉树,否则 ...

  7. 《Linux内核分析》第二周学习报告

    <Linux内核分析>第二周学习报告 ——操作系统是如何工作的 姓名:王玮怡  学号:20135116 第一节 函数调用堆栈 一.三个法宝 二.深入理解函数调用堆栈 三.参数传递与局部变量 ...

  8. grunt入门讲解1:grunt的基本概念和使用

    Grunt和 Grunt 插件是通过 npm 安装并管理的,npm是 Node.js 的包管理器. Grunt 0.4.x 必须配合Node.js >= 0.8.0版本使用.老版本的 Node. ...

  9. 重温redis命令

    redis是已知的性能最快的key-value 数据库. 1.key相关命令 exists key :检查指定的key是否存在 1表示存在 0表示不存在 del key1,key2,key3....: ...

  10. 4 vuex的安装

    安装可以看,引入又问题https://blog.csdn.net/u014196765/article/details/78022065?locationNum=9&fps=1(引入) htt ...