splay就是一种将树上的一个节点经过旋转到节点的一种操作,来保持树的平衡
splay本质:二叉搜索树
特点:结点x的左子树权值都小于x的权值,右子树权值都大于x的权值
- 如果当前处于共线状态的话,那么先旋转y,再旋转x.这样可以强行让他们不处于共线状态,然后平衡这棵树.
- 如果当前不是共线状态的话,那么只要旋转x即可.
普通平衡树
基本上每个平台都有的模板题。
为什么要splay?仅仅是插入不是插进去就行吗?成不成为根节点有什么关系?
这是为了查找比x小/大的第一个数做铺垫,因为有可能x在树中没有出现过,所以先插入x,再找前驱/后继,这就可以直接从根节点找起)
splay的旋转操作
void pushup(int u)
{
tr[u].size=tr[tr[u].s[1]].size+tr[tr[u].s[0]].size+tr[u].cnt;
}
void rotate(int x)
{
int y=tr[x].p,z=tr[y].p;
int k=tr[y].s[1]==x;
tr[z].s[tr[z].s[1]==y]=x,tr[x].p=z;//第一条线
tr[y].s[k]=tr[x].s[k^1],tr[tr[x].s[k^1]].p=y;//第三条线
tr[x].s[k^1]=y,tr[y].p=x;//第2条线
pushup(y),pushup(x);
}
splay操作
void splay(int x,int goal)//将x旋转为goal的儿子,如果goal是0则旋转到根
{
while(t[x].ff!=goal)//一直旋转到x成为goal的儿子
{
int y=t[x].ff,z=t[y].ff;//父节点祖父节点
if(z!=goal)//如果Y不是根节点,则分为上面两类来旋转
(t[z].ch[0]==y)^(t[y].ch[0]==x)?rotate(x):rotate(y);//判断共线还是不共线
//这就是之前对于x和y是哪个儿子的讨论
rotate(x);//无论怎么样最后的一个操作都是旋转x
}
if(goal==0)root=x;//如果goal是0,则将根节点更新为x
}
查找find操作
从根节点开始,左侧都比他小,右侧都比他大,
所以只需要相应的往左/右递归
如果当前位置的val已经是要查找的数
那么直接把他Splay到根节点,方便接下来的操作
void find(int x)//查找x的位置,并将其旋转到根节点
{
int u=root;
if(!u) return ;//树空
while(tr[u].s[x>tr[u].v]&&x!=tr[u].v)//当存在儿子并且当前位置的值不等于x
u=tr[u].s[x>tr[u].v];//跳转到儿子,查找x的父节点
splay(u,0);//把当前位置旋转到根节点
}
Insert操作
往Splay中插入一个数
类似于Find操作,只是如果是已经存在的数,就可以直接在查找到的节点的进行计数
如果不存在,在递归的查找过程中,会找到他的父节点的位置,
然后就会发现底下没有啦。。。
所以这个时候新建一个节点就可以了
inline void insert(int x)//插入x
{
int u=root,ff=0;//当前位置u,u的父节点ff
while(u&&t[u].val!=x)//当u存在并且没有移动到当前的值
{
ff=u;//向下u的儿子,父节点变为u
u=t[u].ch[x>t[u].val];//大于当前位置则向右找,否则向左找
}
if(u)//存在这个值的位置
t[u].cnt++;//增加一个数
else//不存在这个数字,要新建一个节点来存放
{
u=++tot;//新节点的位置
if(ff)//如果父节点非根
t[ff].ch[x>t[ff].val]=u;
t[u].ch[0]=t[u].ch[1]=0;//不存在儿子
t[tot].ff=ff;//父节点
t[tot].val=x;//值
t[tot].cnt=1;//数量
t[tot].size=1;//大小
}
splay(u,0);//把当前位置移到根,保证结构的平衡
}
前驱/后继操作Next
首先就要执行Find操作
把要查找的数弄到根节点
然后,以前驱为例
先确定前驱比他小,所以在左子树上
然后他的前驱是左子树中最大的值
所以一直跳右结点,直到没有为止
找后继反过来就行了
int Next(int x,int f)//查找x的前驱(0)或者后继(1)
{
find(x);
int u=root;//根节点,此时x的父节点(存在的话)就是根节点
if(t[u].val>x&&f)return u;//如果当前节点的值大于x并且要查找的是后继
if(t[u].val<x&&!f)return u;//如果当前节点的值小于x并且要查找的是前驱
u=t[u].ch[f];//查找后继的话在右儿子上找,前驱在左儿子上找
while(t[u].ch[f^1])u=t[u].ch[f^1];//要反着跳转,否则会越来越大(越来越小)
return u;//返回位置
}
删除操作
现在就很简单啦
首先找到这个数的前驱,把他Splay到根节点
然后找到这个数后继,把他旋转到前驱的底下
比前驱大的数是后继,在右子树
比后继小的且比前驱大的有且仅有当前数
在后继的左子树上面,
因此直接把当前根节点的右儿子的左儿子删掉就可以啦
void dele(int x)
{
int last=Next(x,0);//找到前驱
int next=Next(x,1);//找的后继
splay(last,0),splay(next,last);
//将前驱旋转到根节点,后继旋转到根节点下面
//很明显,此时后继是前驱的右儿子,x是后继的左儿子,并且x是叶子节点
int del=tr[next].s[0];//后继的左儿子
if(tr[del].cnt>1)//如果超过一个
{
tr[del].cnt--;
splay(del,0);
}
else tr[next].s[0]=0;//这个节点直接丢掉(不存在了)
}
查找第K大
从当前根节点开始,检查左子树大小
因为所有比当前位置小的数都在左侧
如果左侧的数的个数多余K,则证明第K大在左子树中
否则,向右子树找,找K-左子树大小-当前位置的数的个数
记住特判K恰好在当前位置
int kth(int x)//查找排名为x的数
{
int u=root;
if(tr[u].size<x)//如果当前树上没有这么多数
return 0;//不纯在
while(true)
{
int y=tr[u].s[0];//左儿子
if(x>tr[y].size+tr[u].cnt)
{//如果排名比左儿子的大小和当前节点的数量要大
x-=tr[y].size+tr[u].cnt;
u=tr[u].s[1];
}else if(tr[y].size>=x)//否则的话在当前节点或者左儿子上查找
u=y;
else //否则就是在当前根节点上
return tr[u].v;
}
}
完整代码
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
struct node
{
int s[2];
int p,v,cnt,size;
}tr[N*4];
int idx,n,m,root;
void pushup(int u)
{
tr[u].size=tr[tr[u].s[1]].size+tr[tr[u].s[0]].size+tr[u].cnt;
}
void rotate(int x)
{
int y=tr[x].p,z=tr[y].p;
int k=tr[y].s[1]==x;
tr[z].s[tr[z].s[1]==y]=x,tr[x].p=z;
tr[y].s[k]=tr[x].s[k^1],tr[tr[x].s[k^1]].p=y;
tr[x].s[k^1]=y,tr[y].p=x;
pushup(y),pushup(x);
}
void splay(int x,int k)
{
while(tr[x].p!=k)
{
int y=tr[x].p,z=tr[y].p;
if(z!=k)
if((tr[y].s[1]==x)^(tr[z].s[1]==y)) rotate(x);
else rotate(y);
rotate(x);
}
if(!k) root=x;
}
void find(int x)
{
int u=root;
if(!u) return ;
while(tr[u].s[x>tr[u].v]&&x!=tr[u].v)
u=tr[u].s[x>tr[u].v];
splay(u,0);
}
void insert(int x)
{
int u=root,p=0;
while(u&&tr[u].v!=x)
p=u,u=tr[u].s[x>tr[u].v];
if(u) tr[u].cnt++;
else
{
u=++idx;
if(p) tr[p].s[x>tr[p].v]=u;
tr[u].s[1]=tr[u].s[0]=0;
tr[u].p=p;
tr[u].size=1;
tr[u].cnt=1;
tr[u].v=x;
}
splay(u,0);
}
int kth(int x)//查找排名为x的数
{
int u=root;
if(tr[u].size<x)//如果当前树上没有这么多数
return 0;//不纯在
while(true)
{
int y=tr[u].s[0];//左儿子
if(x>tr[y].size+tr[u].cnt)
{//如果排名比左儿子的大小和当前节点的数量要大
x-=tr[y].size+tr[u].cnt;
u=tr[u].s[1];
}else if(tr[y].size>=x)//否则的话在当前节点或者左儿子上查找
u=y;
else //否则就是在当前根节点上
return tr[u].v;
}
}
int Next(int x,int f)
{
find(x);
int u=root;
if(tr[u].v>x&&f) return u;
if(tr[u].v<x&&!f) return u;
u=tr[u].s[f];//查找后继的话在右儿子上找,前驱在左儿子上找
while(tr[u].s[f^1])u=tr[u].s[f^1];//要反着跳转,否则会越来越大(越来越小)
return u;
}
void dele(int x)
{
int last=Next(x,0);//找到前驱
int next=Next(x,1);//找的后继
splay(last,0),splay(next,last);
//将前驱旋转到根节点,后继旋转到根节点下面
//很明显,此时后继是前驱的右儿子,x是后继的左儿子,并且x是叶子节点
int del=tr[next].s[0];//后继的左儿子
if(tr[del].cnt>1)//如果超过一个
{
tr[del].cnt--;
splay(del,0);
}
else tr[next].s[0]=0;//这个节点直接丢掉(不存在了)
}
int main()
{
cin>>n;
insert(-1e9);
insert(1e9);
while(n--)
{
int op,x;
cin>>op>>x;
if(op==1)
insert(x);
else if(op==2)
dele(x);
else if(op==3)
{
find(x);
printf("%d\n",tr[tr[root].s[0]].size);
}else if(op==4)
{
printf("%d\n",kth(x+1));
}
else if (op==5)
printf("%d\n",tr[Next(x,0)].v);
if (op==6)
printf("%d\n",tr[Next(x,1)].v);
}
return 0;
}