平衡树实际很简单的

以下讲解都以Luogu P3369 【模板】普通平衡树为例

我不会带指针的Splay,所以我就写非指针型的Splay

Splay是基于二叉查找树(bst)实现的

什么是二叉查找树呢?就是一棵树呗,但是这棵树满足性质:一个节点的左孩子一定比它小,右孩子一定比它大

比如:

这就是一棵最基本二叉查找树

对于每次插入,它的期望复杂度大约是log^2 n级别的,但是存在极端情况,比如9999999 9999998 9999997.....1这种数据,会直接被卡成n^2级别

在这种情况下,平衡树出现了!

1.定义Splay

struct node
{
int v;//权值
int fa;//父亲节点
int ch[2];//0代表左儿子,1代表右儿子
int rec;//这个权值的节点出现的次数
int sum;//子节点的数量
}tree[N];//N为节点最多有多少
int tot;//tot表示不算重复的有多少节点

2.Splay的核心

Rotate

首先考虑一下,我们要把一个点挪到根,那我们首先要知道怎么让一个点挪到它的父节点

情况1:

当X是Y的左孩子时

ABC实际可以是子树,但这里假设ABC都是点

这时候如果我们让X成为Y的父亲,只会影响到3组点的关系

B与X,X与Y,X与R

根据二叉排序树的性质

B会成为Y的左儿子

Y会成为X的右儿子

X会成为R的儿子,具体是什么儿子,这个要看Y是R的啥儿子

情况2:

当X是Y的右孩子

本质上是和情况1一样的qaq

旋转后变成

能不能把这两种情况合并呢qaq?结果是肯定的

我们需要一个函数来确定这个节点是他父节点的左孩子还是右孩子

inline bool findd(register int x)
{
return tree[tree[x].fa].ch[0]==x?0:1;
}

如果是左孩子的话会返回0,右孩子会返回1

那么我们不难得到R,Y,X这三个节点的信息

int Y=tree[x].fa;
int R=tree[Y].fa;
int Yson=findd(x);
int Rson=findd(Y);

B的情况我们可以根据X的情况推算出来,根据运算的性质,01=1,11=0,21=3,3^1=2,而且B相对于X的位置一定是与X相对于Y的位置是相反的

(否则在旋转的过程中不会对B产生影响)

int B=tree[x].ch[Yson^1];

然后我们考虑连接的过程

根据上面的图,不难得到(自己向上翻qaq)

1.B成为Y的哪个儿子与X是Y的哪个儿子是一样的

2.Y成为X的哪个儿子与X是Y的哪个儿子相反

3.X成为R的哪个儿子与Y是R的哪个儿子相同

connect(B,Y,Yson);
connect(Y,x,Yson^1);
connect(x,R,Rson);

connect函数也很好写

inline void connect(register int x,register int fa,register int son) //把x转为fa的son(son是0/1,表示左孩子或右孩子)
{
tree[x].fa=fa;
tree[fa].ch[son]=x;
}

Rotate代码总览(旋转完不要忘了update):

inline void update(register int x)
{
tree[x].sum=tree[tree[x].ch[0]].sum+tree[tree[x].ch[1]].sum+tree[x].rec;
}
inline bool findd(register int x)
{
return tree[tree[x].fa].ch[0]==x?0:1;
}
inline void connect(register int x,register int fa,register int son) //把x转为fa的son(son是0/1,表示左孩子或右孩子)
{
tree[x].fa=fa;
tree[fa].ch[son]=x;
}
inline void rotate(register int x)
{
int Y=tree[x].fa;
int R=tree[Y].fa;
int Yson=findd(x);
int Rson=findd(Y);
int B=tree[x].ch[Yson^1];
connect(B,Y,Yson);
connect(Y,x,Yson^1);
connect(x,R,Rson);
update(Y),update(x);
}

Splay

Splay(x,to)是实现把x节点搬到to节点

最简单的办法,对于x这个节点,每次上旋直到to

但是!

毒瘤的出题人可以构造数据把上面的这种方法卡到n^2 qaq*

下面我们介绍一下双旋的Splay

这里的情况有很多,但是总的来说就三种情况

1.to是x的爸爸,

这样的话吧x旋转上去就好

if(tree[tree[x].fa].fa==to)
rotate(x);

2.x和他爸爸和他爸爸的爸爸在一条线上(文字游戏)

其实就是findd(x)=findd(tree[x].fa)

这时候先把Y旋转上去,再把X旋转上去就好

if(findd(x)==find(tree[x].fa))
rotate(tree[x].fa),rotate(x);

3.x和他爸爸和他爸爸的爸爸不在一条线上(和2相反)

这时候把X旋转两次就好

spaly函数的代码:

inline void splay(register int x,register int to)
{
to=tree[to].fa;
while(tree[x].fa!=to)
{
int y=tree[x].fa;
if(tree[y].fa==to)
rotate(x);
else if(findd(x)==findd(y))
rotate(y),rotate(x);
else
rotate(x),rotate(x);
}
}

Splay的核心代码到此结束

剩下的就是一些其他的东西(虽说有的也挺重要qaq)

3.其他的一些函数

insert

根据前面讲的,我们在插入一个数之后,需要将其旋转到根

首先,当这棵树已经没有节点的时候,我们直接新建一个节点就好

inline int newpoint(register int v,register int fa)
{
tree[++tot].fa=fa;
tree[tot].v=v;
tree[tot].sum=tree[tot].rec=1;
return tot;
}

然后,当这可树有节点的时候,我们根据二叉查找树的性质,不断向下走,直到找到一个可以插入的点,注意在走的时候需要更新一个每个节点的sum值

inline void Insert(register int x)
{
int now=tree[0].ch[1];
if(tree[0].ch[1]==0)
{
newpoint(x,0);
tree[0].ch[1]=tot;
}
else
{
while(19260817)
{
++tree[now].sum;
if(tree[now].v==x)
{
++tree[now].rec;
splay(now,tree[0].ch[1]);
return;
}
int nxt=x<tree[now].v?0:1;
if(!tree[now].ch[nxt])
{
int p=newpoint(x,now);
tree[now].ch[nxt]=p;
splay(p,tree[0].ch[1]);
return;
}
now=tree[now].ch[nxt];
}
}
}

delete

删除的功能是:删除权值为v的节点

我们不难想到:我们可以先找到他的位置,再把这个节点删掉

找位置用find函数,不要和rotate的findd搞混

inline int find(register int v)
{
int now=tree[0].ch[1];
while(19260817)
{
if(tree[now].v==v)
{
splay(now,tree[0].ch[1]);
return now;
}
int nxt=v<tree[now].v?0:1;
if(!tree[now].ch[nxt])
return 0;
now=tree[now].ch[nxt];
}
}

下面我们需要删除函数

怎么样才能保证删除节点后整棵树还满足二叉查找树的性质

此时会出现几种情况

1.权值为v的节点已经出现过
这时候直接把他的rec和sum减去1就好
2.本节点没有左右儿子
这样的话就成了一棵空树
3.本节点没有左儿子
直接把他的右儿子设置成根
4.既有左儿子,又有右儿子
在它的左儿子中找到最大的,旋转到根,把它的右儿子当做根(也就是它最大的左儿子)的右儿子

最后把这个节点删掉就好

delete的代码

inline void delet(register int x)
{
int pos=find(x);
if(!pos)
return;
if(tree[pos].rec>1)
{
--tree[pos].rec;
--tree[pos].sum;
}
else
{
if(!tree[pos].ch[0]&&!tree[pos].ch[1])
tree[0].ch[1]=0;
else if(!tree[pos].ch[0])
{
tree[0].ch[1]=tree[pos].ch[1];
tree[tree[0].ch[1]].fa=0;
}
else
{
int left=tree[pos].ch[0];
while(tree[left].ch[1])
left=tree[left].ch[1];
splay(left,tree[pos].ch[0]);
connect(tree[pos].ch[1],left,1);
connect(left,0,1);
update(left);
}
}
}

rank

1.查询x数的排名

十分简短

inline int rank(register int v)
{
int pos=find(v);
return tree[tree[pos].ch[0]].sum+1;
}

2.查询排名为x的数

这个操作就是上面那个操作的逆向操作

inline int arank(register int x)
{
int now=tree[0].ch[1];
while(19260817)
{
int used=tree[now].sum-tree[tree[now].ch[1]].sum;
if(x>tree[tree[now].ch[0]].sum&&x<=used)
{
splay(now,tree[0].ch[1]);
return tree[now].v;
}
if(x<used)
now=tree[now].ch[0];
else
x-=used,now=tree[now].ch[1];
}
}

求前驱和后继

前驱

这个更容易,我们可以维护一个ans变量,然后对整棵树进行遍历,同时更新ans

inline int lower(register int v)
{
int now=tree[0].ch[1];
int ans=-inf;
while(now)
{
if(tree[now].v<v&&tree[now].v>ans)
ans=tree[now].v;
if(v>tree[now].v)
now=tree[now].ch[1];
else
now=tree[now].ch[0];
}
return ans;
}

后继

和前驱差不多

inline int upper(register int v)
{
int now=tree[0].ch[1];
int ans=inf;
while(now)
{
if(tree[now].v>v&&tree[now].v<ans)
ans=tree[now].v;
if(v<tree[now].v)
now=tree[now].ch[0];
else
now=tree[now].ch[1];
}
return ans;
}

4.Spaly整体代码

#pragma GCC optimize("O3")
#include <bits/stdc++.h>
#define N 100005
#define inf 1000000005
using namespace std;
inline int read()
{
register int x=0,f=1;register char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
inline void write(register int x)
{
if(!x)putchar('0');if(x<0)x=-x,putchar('-');
static int sta[36];int tot=0;
while(x)sta[tot++]=x%10,x/=10;
while(tot)putchar(sta[--tot]+48);
}
struct node
{
int v;
int fa;
int ch[2];
int rec;
int sum;
}tree[N];
int tot;
inline void update(register int x)
{
tree[x].sum=tree[tree[x].ch[0]].sum+tree[tree[x].ch[1]].sum+tree[x].rec;
}
inline bool findd(register int x)
{
return tree[tree[x].fa].ch[0]==x?0:1;
}
inline void connect(register int x,register int fa,register int son) //把x转为fa的son(son是0/1,表示左孩子或右孩子)
{
tree[x].fa=fa;
tree[fa].ch[son]=x;
}
inline void rotate(register int x)
{
int Y=tree[x].fa;
int R=tree[Y].fa;
int Yson=findd(x);
int Rson=findd(Y);
int B=tree[x].ch[Yson^1];
connect(B,Y,Yson);
connect(Y,x,Yson^1);
connect(x,R,Rson);
update(Y),update(x);
}
inline void splay(register int x,register int to)
{
to=tree[to].fa;
while(tree[x].fa!=to)
{
int y=tree[x].fa;
if(tree[y].fa==to)
rotate(x);
else if(findd(x)==findd(y))
rotate(y),rotate(x);
else
rotate(x),rotate(x);
}
}
inline int newpoint(register int v,register int fa)
{
tree[++tot].fa=fa;
tree[tot].v=v;
tree[tot].sum=tree[tot].rec=1;
return tot;
}
inline void Insert(register int x)
{
int now=tree[0].ch[1];
if(tree[0].ch[1]==0)
{
newpoint(x,0);
tree[0].ch[1]=tot;
}
else
{
while(19260817)
{
++tree[now].sum;
if(tree[now].v==x)
{
++tree[now].rec;
splay(now,tree[0].ch[1]);
return;
}
int nxt=x<tree[now].v?0:1;
if(!tree[now].ch[nxt])
{
int p=newpoint(x,now);
tree[now].ch[nxt]=p;
splay(p,tree[0].ch[1]);
return;
}
now=tree[now].ch[nxt];
}
}
}
inline int find(register int v)
{
int now=tree[0].ch[1];
while(19260817)
{
if(tree[now].v==v)
{
splay(now,tree[0].ch[1]);
return now;
}
int nxt=v<tree[now].v?0:1;
if(!tree[now].ch[nxt])
return 0;
now=tree[now].ch[nxt];
}
}
inline void delet(register int x)
{
int pos=find(x);
if(!pos)
return;
if(tree[pos].rec>1)
{
--tree[pos].rec;
--tree[pos].sum;
}
else
{
if(!tree[pos].ch[0]&&!tree[pos].ch[1])
tree[0].ch[1]=0;
else if(!tree[pos].ch[0])
{
tree[0].ch[1]=tree[pos].ch[1];
tree[tree[0].ch[1]].fa=0;
}
else
{
int left=tree[pos].ch[0];
while(tree[left].ch[1])
left=tree[left].ch[1];
splay(left,tree[pos].ch[0]);
connect(tree[pos].ch[1],left,1);
connect(left,0,1);
update(left);
}
}
}
inline int rank(register int v)
{
int pos=find(v);
return tree[tree[pos].ch[0]].sum+1;
}
inline int arank(register int x)
{
int now=tree[0].ch[1];
while(19260817)
{
int used=tree[now].sum-tree[tree[now].ch[1]].sum;
if(x>tree[tree[now].ch[0]].sum&&x<=used)
{
splay(now,tree[0].ch[1]);
return tree[now].v;
}
if(x<used)
now=tree[now].ch[0];
else
x-=used,now=tree[now].ch[1];
}
}
inline int lower(register int v)
{
int now=tree[0].ch[1];
int ans=-inf;
while(now)
{
if(tree[now].v<v&&tree[now].v>ans)
ans=tree[now].v;
if(v>tree[now].v)
now=tree[now].ch[1];
else
now=tree[now].ch[0];
}
return ans;
}
inline int upper(register int v)
{
int now=tree[0].ch[1];
int ans=inf;
while(now)
{
if(tree[now].v>v&&tree[now].v<ans)
ans=tree[now].v;
if(v<tree[now].v)
now=tree[now].ch[0];
else
now=tree[now].ch[1];
}
return ans;
}
int main()
{
int m=read();
while(m--)
{
int opt=read(),x=read();
if(opt==1)
Insert(x);
else if(opt==2)
delet(x);
else if(opt==3)
{
write(rank(x));
printf("\n");
}
else if(opt==4)
{
write(arank(x));
printf("\n");
}
else if(opt==5)
{
write(lower(x));
printf("\n");
}
else
{
write(upper(x));
printf("\n");
}
}
return 0;
}

5.相关题目

1.Luogu P2234 [HNOI2002]营业额统计

平衡树板题

2.Luogu P1503 鬼子进村

平衡树板题

3.Luogu P3871 [TJOI2010]中位数

平衡树板题

4.Luogu P1533 可怜的狗狗

莫队+平衡树苟过

5.Luogu P2073 送花

平衡树板题

以上是splay的基本应用qaq

还有一种操作没讲,就是如何进行区间操作

实现起来很简单

假设我们要在[l,r]之间上搞事情,我们首先把l的前驱旋转到根节点,再把r的后继转到根节点的右儿子

那么此时根节点右儿子的左儿子代表的就是区间[l,r]

这应该很好理解qaq

然后就可以像线段树的lazy标记一样,给区间l,rl,r打上标记,延迟更新,比如区间反转的时候更新的时候直接交换左右儿子

我们下面以P3391 【模板】文艺平衡树(Splay)为例

这里有一个奇技淫巧:如果一个区间被打了两次,那么就相当于不打

所以我们用一个bool变量来储存该节点是否需要被旋转

pushdown函数可以这么写(rev就是翻转标记)

inline void pushdown(register int x)
{
if(tree[x].rev)
{
swap(tree[x].ch[0],tree[x].ch[1]);
tree[tree[x].ch[0]].rev^=1;
tree[tree[x].ch[1]].rev^=1;
tree[x].rev=0;
}
}

这道题完整代码

#include <bits/stdc++.h>
#define N 100005
#define inf 0x7fffff
using namespace std;
inline int read()
{
register int x=0,f=1;register char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
inline void write(register int x)
{
if(!x)putchar('0');if(x<0)x=-x,putchar('-');
static int sta[36];int cnt=0;
while(x)sta[cnt++]=x%10,x/=10;
while(cnt)putchar(sta[--cnt]+48);
}
int n,m;
struct node{
int fa,ch[2],tot;
bool rev;
}tree[N];
int root,PosL,PosR;
inline bool findd(register int x)
{
return x==tree[tree[x].fa].ch[1];
}
inline void connect(register int x,register int fa,register int son)
{
tree[x].fa=fa;
tree[fa].ch[son]=x;
}
inline void update(register int x)
{
tree[x].tot=tree[tree[x].ch[0]].tot+tree[tree[x].ch[1]].tot+1;
}
inline void rotate(register int x)
{
int Y=tree[x].fa;
if(Y==root)
root=x;
int R=tree[Y].fa;
int Yson=findd(x);
int Rson=findd(Y);
int B=tree[x].ch[Yson^1];
connect(B,Y,Yson);
connect(Y,x,Yson^1);
connect(x,R,Rson);
update(Y);
update(x);
}
inline void splay(register int x,register int to)
{
while(tree[x].fa!=to)
{
int y=tree[x].fa;
if(tree[y].fa==to)
rotate(x);
else if(findd(x)==findd(y))
rotate(y),rotate(x);
else
rotate(x),rotate(x);
}
update(x);
}
inline int buildsplay(register int l,register int r)
{
if(l>r)
return 0;
int mid=l+r>>1;
connect(buildsplay(l,mid-1),mid,0);
connect(buildsplay(mid+1,r),mid,1);
tree[mid].rev=0;
update(mid);
return mid;
}
inline void pushdown(register int x)
{
if(tree[x].rev)
{
swap(tree[x].ch[0],tree[x].ch[1]);
tree[tree[x].ch[0]].rev^=1;
tree[tree[x].ch[1]].rev^=1;
tree[x].rev=0;
}
}
inline int find(register int x)
{
int now=root;
--x;
pushdown(now);
while(x!=tree[tree[now].ch[0]].tot)
{
if(tree[tree[now].ch[0]].tot<x)
x-=tree[tree[now].ch[0]].tot+1,now=tree[now].ch[1];
else
now=tree[now].ch[0];
pushdown(now);
}
return now;
}
inline void print(register int now)
{
if(!now)
return;
pushdown(now);
print(tree[now].ch[0]);
if(now!=1&&now!=n+2)
write(now-1),putchar(' ');
print(tree[now].ch[1]);
}
int main()
{
n=read(),m=read();
root=buildsplay(1,n+2);
while(m--)
{
int l=read(),r=read();
PosL=find(l);
splay(PosL,0);
PosR=find(r+2);
splay(PosR,root);
tree[tree[PosR].ch[0]].rev^=1;
}
print(root);
return 0;
}

Splay详解的更多相关文章

  1. 在洛谷3369 Treap模板题 中发现的Splay详解

    本题的Splay写法(无指针Splay超详细) 前言 首先来讲...终于调出来了55555...调了整整3天..... 看到大部分大佬都是用指针来实现的Splay.小的只是按照Splay的核心思想和原 ...

  2. splay详解(一)

    前言 Spaly是基于二叉查找树实现的, 什么是二叉查找树呢?就是一棵树呗:joy: ,但是这棵树满足性质—一个节点的左孩子一定比它小,右孩子一定比它大 比如说 这就是一棵最基本二叉查找树 对于每次插 ...

  3. splay详解(二)

    前言 在上一节中,我们讲述了Splay的核心操作rotate与splay 本节我会教大家如何用这两个函数实现各种强大的功能 为了方便讲解,我们拿这道题做例题来慢慢分析 利用splay实现各种功能 首先 ...

  4. splay详解(三)

    前言 上一节我们学习了splay所能解决的基本问题,这节我来讲一下splay怎么搞区间问题 实现 splay搞区间问题非常简单,比如我们要在区间$l,r$上搞事情,那么我们首先把$l$的前驱旋转到根节 ...

  5. 普通Splay详解

    预备知识: 二叉搜索树(BST) 至于BST,随便看一下就可以, 我们知道二叉搜索树是O(logN)的,那我们为什么要用平衡树呢? 之前我们了解到,BST的插入是小的往左子树走,大的往右子树走,如果凉 ...

  6. [转载]Splay Tree数组实现+详解

    变量声明:f[i]表示i的父结点,ch[i][0]表示i的左儿子,ch[i][1]表示i的右儿子,key[i]表示i的关键字(即结点i代表的那个数字),cnt[i]表示i结点的关键字出现的次数(相当于 ...

  7. Link-Cut-Tree详解

    图片参考YangZhe的论文,FlashHu大佬的博客 Link-Cut-Tree实际靠的是实链剖分,重链剖分和长链剖分珂以参考树链剖分详解 Link-Cut-Tree将某一个儿子的连边划分为实边,而 ...

  8. Linq之旅:Linq入门详解(Linq to Objects)

    示例代码下载:Linq之旅:Linq入门详解(Linq to Objects) 本博文详细介绍 .NET 3.5 中引入的重要功能:Language Integrated Query(LINQ,语言集 ...

  9. 架构设计:远程调用服务架构设计及zookeeper技术详解(下篇)

    一.下篇开头的废话 终于开写下篇了,这也是我写远程调用框架的第三篇文章,前两篇都被博客园作为[编辑推荐]的文章,很兴奋哦,嘿嘿~~~~,本人是个很臭美的人,一定得要截图为证: 今天是2014年的第一天 ...

随机推荐

  1. css背景图撑开盒子高度

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  2. An error occurred. Sorry, the page you are looking for is currently unavailable. Please try again later.

    刚装完 PHP.Nginx,准备跑下 phpMyAdmin 程序,结果报以下错误: An error occurred. Sorry, the page you are looking for is ...

  3. Nest.js 拦截器

    Docs: https://docs.nestjs.com/interceptors 该对象包含从路由处理程序返回的值 在方法执行之前/之后绑定额外的逻辑 转换函数返回的结果 转换从函数抛出的异常 / ...

  4. 查看oracle数据库是否为归档模式

    查看oracle数据库是否为归档模式   [1]   1.select name,log_mode from v$database;   NAME LOG_MODE   --------------- ...

  5. Coffee and Coursework (Easy version)

    Coffee and Coursework (Easy version) time limit per test 1 second memory limit per test 256 megabyte ...

  6. 前端自动化构建工具webpack (一)之webpack安装 和 设置webpack.confi.js

    目的:  模块化开发,组件化开发,让代码符合开发规范,更高效 webpack作用:从pack,就知道这是包的意思,比较专业点叫做前端自动化打包构建工具,代码错误检查,预处理,文件合并(打包),代码压缩 ...

  7. 剑指offer——python【第34题】第一个只出现一次的字符

    题目描述 在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写) 思路 遍历字符串,找到那个第 ...

  8. 剑指offer——python【第56题】删除链表中的重复节点

    题目描述 在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针. 例如,链表1->2->3->3->4->4->5 处理后 ...

  9. js设计模式(五)---观察者模式

    概述: 观察者模式也叫 “ 发布-订阅 " 模式 , 发布者发布信息是不需要考虑订阅者是谁?添加订阅者的时候也不需要通知发布者. 应用: 最经典的就是: DOM事件 开发过程中我们常用自定义 ...

  10. 页面初始化document.body.clientWidth大小变化

    目前:原因不明 初步判断:设置字体大小前图片加载失败! 结果:等待验证