Splay模板讲解及一些题目
简介
平衡二叉树(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具有这样的性质:
- 一个节点的权值总比它的左儿子大,比它的右儿子小.
- 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还有一些操作:
- 插入一个数字.
- 删除一个数字.
- 查询一个数字的排名.
- 查找一个数字的前驱/后继.
- 查询第k小的数字是多少.
- 查询最值.
首先我们来看如何插入一个数字.
插入节点时是按照新插入的节点权值来遍历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
- [HNOI2002]营业额统计
- [NOI2004]郁闷的出纳员
- [JSOI2008]最大数
- [NOI2003]文本编辑器
- [ZJOI2006]书架
- [HNOI2004]宠物收养场
- [HNOI2012]永无乡
- [NOI2005]维护数列
题解
T1:
动态查询前驱并统计答案,没什么好讲的吧.
T2:
对于修改所有人的工资,可以直接用变量保存所有人被修改的工资而不用一个个修改.然后在查询的时候直接找到第一个比劝退标准低的人,删除的时候直接把它以及它左边的全部删掉.也就是在删除的过程中找到它以及它子树的位置旋到根节点的右儿子的左儿子,然后直接删除它的父指针.
T3:
插入的时候直接插入到树的最右边,这样维护的一颗splay的中序遍历结果就是这个序列了.然后在节点维护一个最大值,查找的时候就先找到它前面一个元素的排名旋转到根,那么根节点的右儿子的最大值就是答案了.
T4:
按照题意模拟,可以在插入一个序列的时候先将这个序列处理成一棵树然后再合并.
T5:
可以考虑给每个元素定义一个优先值来维护平衡树的性质(反正当时我做这道题的时候老是搞不清,就这么写了).用一个数组记录一本书的编号映射到树中的优先值.
- 对于要放到书架顶端的书,先将它删除,然后再给它赋一个最小值插入树中.
- 对于要放到书架底端的书同理.
- 对于要与前驱后继交换的书,先交换编号映射的优先值,然后再分别删除,插入这两个点.
- 其他直接模板操作解决.
T6:
因为没有领养者的时候来领养者,或是没有宠物的时候来宠物,都会找到目前树中与该值最接近的一个(前驱后继中取min),所以考虑用一个计数器统计当前领养者/宠物数,来表示目前状态的树为宠物树/领养者树,然后再对这些情况分类讨论一下就可以了.
T7:
链接
<\br>
T8:
按照题意模拟...注意细节,具体代码实现可以戳这里
Splay模板讲解及一些题目的更多相关文章
- bzoj 1588 splay模板题
用晚自习学了一下splay模板,没想象中那么难,主要是左旋和右旋可以简化到一个函数里边,减少代码长度... #include<iostream> #include<cstdio> ...
- COJ 1002 WZJ的数据结构(二)(splay模板)
我的LCC,LCT,Splay格式终于统一起来了... 另外..这个形式的Splay是标准的Splay(怎么鉴别呢?看Splay函数是否只传了一个变量node就行),刘汝佳小白书的Splay写的真是不 ...
- Splay 模板
Splay 模板 struct SplayTree{ const static int maxn = 1e5 + 15; int ch[maxn][2] , key[maxn] , s[maxn] , ...
- [luogu3369/bzoj3224]普通平衡树(splay模板、平衡树初探)
解题关键:splay模板题整理. 如何不加入极大极小值?(待思考) #include<cstdio> #include<cstring> #include<algorit ...
- BZOJ1588 [HNOI2002]营业额统计 splay模板
1588: [HNOI2002]营业额统计 Time Limit: 5 Sec Memory Limit: 162 MB Submit: 16189 Solved: 6482 [Submit][S ...
- 文艺平衡树(splay模板)
题干:splay模板,要求维护区间反转. splay是一种码量小于treap,但支持排名,前驱后继等treap可求的东西,也支持区间反转的平衡树. 但是有两个坏处: 1.splay常数远远大于trea ...
- [洛谷P3391] 文艺平衡树 (Splay模板)
初识splay 学splay有一段时间了,一直没写...... 本题是splay模板题,维护一个1~n的序列,支持区间翻转(比如1 2 3 4 5 6变成1 2 3 6 5 4),最后输出结果序列. ...
- POJ 3481 splay模板
最后撸一发splay. 之前用treap撸的,现在splay也找到感觉了,果然不同凡响,两者之间差别与精妙之处各有其精髓! 真心赞一个! POJ平衡树的题目还是比较少,只能挑之前做过的捏一捏.但是收获 ...
- bzoj3224 普通平衡树 splay模板
题目传送门 题目大意:完成一颗splay树. 思路:模板题,学着还是很有意思的. 学习splay树:蒟蒻yyb 该题模板:汪立超 #include<bits/stdc++.h> #defi ...
随机推荐
- 一款基于Zigbee技术的智慧鱼塘系统研究与设计
在现代鱼塘养鱼中,主要困扰渔农的就是养殖成本问题.而鱼塘养殖成本最高的就是养殖的人工费,喂养的饲料费和鱼塘中高达几千瓦增氧机的消耗的电费.实现鱼塘自动化养殖将会很好地解决上述问题,大大提高渔农的经济效 ...
- SPIR-V*:面向 OpenCL™ 工作负载的英特尔® 显卡编译器默认接口
英特尔® 显卡编译器最近从 SPIR* 转换到 SPIR-V*,作为面向 OpenCL™ 工作负载的中间表示.这看起来像编译器的内部变化,对用户来说不可见,但是这展示了我们支持 Khronos* 开放 ...
- Docker swarm集群搭建教程
一.什么是Swarm Swarm这个项目名称特别贴切.在Wiki的解释中,Swarm behavior是指动物的群集行为.比如我们常见的蜂群,鱼群,秋天往南飞的雁群都可以称作Swarm behavio ...
- Hyperledger Fabric网络节点架构
Fabric区块链网络的组成  区块链网络结构图 区块链网络组成 组成区块链网络相关的节点 节点是区块链的通信主体,和区块链网络相关的节点有多种类型:客户端(应用).Peer节点.排序服务(Orde ...
- abcdocker 的博客
技术参考总结 abcdocker 的博客 09月 3 篇 20日 Centos7 图形化创建KVM 10日 Nginx 代理Google 进行*** 10日 mac 安装装逼神器cmatrix 08月 ...
- ElasticSearch读写原理
es 写入数据的工作原理是什么啊?es 查询数据的工作原理是什么啊?底层的 lucene 介绍一下呗?倒排索引了解吗? es 写数据过程 客户端选择一个 node 发送请求过去,这个 node 就是 ...
- PAT甲级题解-1097. Deduplication on a Linked List (25)-链表的删除操作
给定一个链表,你需要删除那些绝对值相同的节点,对于每个绝对值K,仅保留第一个出现的节点.删除的节点会保留在另一条链表上.简单来说就是去重,去掉绝对值相同的那些.先输出删除后的链表,再输出删除了的链表. ...
- C++ 多态Polymorphism 介绍+动态绑定、静态绑定
什么是多态? 多态(polymorphism)一词最初来源于希腊语polumorphos,含义是一种物质的多种形态. 在专业术语中,多态是一种运行时绑定机制(run-time binding) ,通过 ...
- CSAPP lab2 二进制拆弹 binary bombs phase_6
给出对应于7个阶段的7篇博客 phase_1 https://www.cnblogs.com/wkfvawl/p/10632044.htmlphase_2 https://www.cnblogs. ...
- ElasticSearch 2 (7) - 基本概念
ElasticSearch 2 (7) - 基本概念 摘要 ElasticSearch的一些基本核心概念,理解这些概念有助于ElasticSearch的学习 准实时NRT(Near Realtime) ...