基本操作
插入
和二叉查找树一样,但是插入完成后要splay一下(即伸展),伸展操作下面有
void ins(ll x,int fa,int &i){//&i:加上了之后其最后一个亲戚就会和他发生关系了!
if(!i){i=++tot;e[i].w=x;e[i].f=fa;splay(i);return;}
if(x<e[i].w)ins(x,i,e[i].s[0]);//重复的元素存不存看情况,存的话放在左子树
else if(x>e[i].w)ins(x,i,e[i].s[1]);
}
伸展
bool is(int i){return e[e[i].f].s[1]==i;}//判断是左儿子或者右儿子
void spin(int i){//旋转
int fa=e[i].f,g=e[e[i].f].f,p=is(i),h=is(e[i].f);
e[fa].s[p]=e[i].s[!p];e[fa].f=i;e[i].s[!p]=fa;
if(e[fa].s[p])e[e[fa].s[p]].f=fa;
if(e[i].f=g)e[g].s[h]=i;
}
void splay(int i){
while(e[i].f){
if(!e[e[i].f].f)spin(i);//zig/zag
else if(is(i)^is(e[i].f))spin(i),spin(i);//zig-zag/zag-zig
else spin(e[i].f),spin(i);//zig-zig/zag-zag
}
rot=i;
}
查找前驱或后驱
例题:codevs1285/洛谷P2286/bzoj1208 宠物收养所
ll pre(ll x,int i){//找前驱,比x小的最大值
if(!i)return -1e8;
if(x==e[i].w){splay(i);return e[i].w;}
if(e[i].w<x)return max(e[i].w,pre(x,e[i].s[1]));
return pre(x,e[i].s[0]);
}
ll nxt(ll x,int i){//找后驱,比x大的最小值
if(!i)return 1e8;
if(x==e[i].w){splay(i);return e[i].w;}
if(e[i].w>x)return min(e[i].w,nxt(x,e[i].s[0]));
return nxt(x,e[i].s[1]);
}
删除和合并
先把要删除的点splay到根,如果有一个子树为空,那么直接删除,否则找到右子树最左边(最小)的节点,把左子树连接到那个节点的左边,再删除要删的点。
void del(int i){
splay(i);
if(e[i].s[0]*e[i].s[1]==0){rot=e[i].s[0]+e[i].s[1];}//如果有一棵子树为空
else {
int k=e[rot].s[1];
while(e[k].s[0])k=e[k].s[0];//找到右子树最小值
e[k].s[0]=e[rot].s[0];e[e[rot].s[0]].f=k;
rot=e[rot].s[1];
}
e[rot].f=0;//notice!
}
关于第k大/小值
寻找第k大/小值
这样的话还要在splay里面维护一个子树的大小,这样就很方便看左子树或者右子树了!
怎么维护?我只讲讲splay操作里的维护,其他的大家开脑洞维护吧。
push_up函数:
void up(int i){e[i].siz=e[e[i].s[0]].siz+e[e[i].s[1]].siz+1;}
然后在spin函数的最后调用一个up(fa)和一个up(i)即可(顺序不可打乱,因为fa已经变成了i的子节点)
int find(int x,int i){//寻找第x大值
if(e[e[i].s[1]].siz==x-1){splay(i);return e[i].v;}
if(e[e[i].s[1]].siz>=x)return find(x,e[i].s[1]);
else return find(x-e[e[i].s[1]].siz-1,e[i].s[0]);
}
插入到第k个位置
这个嘛,与寻找第k大/小有点像。
例题:codevs1514/洛谷P2596/bzoj1861 书架
void up(int i){e[i].siz=e[e[i].s[0]].siz+e[e[i].s[1]].siz+1;}
void bui(int &i,int wz,int fa)//插入操作
{i=wz;e[i].s[0]=e[i].s[1]=0;e[i].f=fa;e[i].siz=1;splay(i);}
void ins_x(int x,int wz,int fa,int i){//把编号为wz的数插入到第x个位置
if(x==1&&!e[i].s[0])bui(e[i].s[0],wz,i);//可以成为该节点的左儿子
else if(x==e[e[i].s[0]].siz+2&&!e[i].s[1])bui(e[i].s[1],wz,i);//可以成为此节点的右儿子
else if(x<=e[e[i].s[0]].siz+1)ins_x(x,wz,i,e[i].s[0]);
else ins_x(x-e[e[i].s[0]].siz-1,wz,i,e[i].s[1]);
if(i)up(i);//如果也push_up了0号就会出问题
}
序列操作
splay被称为序列之王不是没有理由的,它有很丰富的序列操作。
区间翻转
例题:洛谷P3391,bzoj3223,文艺平衡树
首先我们需要添加1和n+2这两个哨兵节点,1n变成2n+1
然后假如要翻转[l,r],我们先找到第l-1个节点,将其splay到根。后再找到r+1个节点,将其splay到根的右儿子,那么该节点的左子树就是区间[l,r],我们打个懒惰标记(是这么叫的吗???),调用find函数寻找第l-1和第r+1个节点的时候,都是每遍历到一个节点就要下传一次懒惰标记。
懒得贴完整代码。。。
下传懒惰标记:
void pd(int i){
swap(e[i].s[0],e[i].s[1]);
if(e[i].s[0])laz[e[i].s[0]]^=1;
if(e[i].s[1])laz[e[i].s[1]]^=1;
laz[i]=0;
}
翻转函数:
void rev(int l,int r){
int x=find(l,rot),y=find(r+2,rot);//find:寻找第l和r+2个节点
splay(x,0);splay(y,rot);//将x和y节点splay成后面那个的儿子
laz[s[y][0]]^=1;//打标记
}
其他
boshi:splay可以做什么?
树王:什么都可以做
所以splay的神奇是一言道不尽的,在学习了基础操作之后,一定要丰富自己的脑洞来编写真的很难写的其他splay操作
板子题
bzoj1251 序列终结者
区间加,区间翻转,区间最大值
#include<bits/stdc++.h>
using namespace std;
const int N=50005,inf=0x3f3f3f3f;
int read() {
int q=0,w=1;char ch=' ';
while(ch!='-'&&(ch<'0'||ch>'9')) ch=getchar();
if(ch=='-') w=-1,ch=getchar();
while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
return q*w;
}
int n,m,rt;
int f[N],son[N][2],siz[N],mx[N],v[N],rev[N],laz[N];
void up(int x) {
mx[x]=max(mx[son[x][0]],mx[son[x][1]]);
mx[x]=max(mx[x],v[x]);
siz[x]=siz[son[x][0]]+siz[son[x][1]]+1;
}
void pd(int x) {
int l=son[x][0],r=son[x][1];
if(laz[x]) {//注意其没有左右儿子的情况
if(l) laz[l]+=laz[x],mx[l]+=laz[x],v[l]+=laz[x];
if(r) laz[r]+=laz[x],mx[r]+=laz[x],v[r]+=laz[x];
laz[x]=0;
}
if(rev[x]) rev[l]^=1,rev[r]^=1,swap(son[x][0],son[x][1]),rev[x]=0;
}
int is(int x) {return son[f[x]][1]==x;}
void spin(int x,int &mb) {
int fa=f[x],g=f[fa],t=is(x);
if(fa==mb) mb=x;
else son[g][is(fa)]=x;//我要当你爸爸的儿子
f[x]=g,f[fa]=x,f[son[x][t^1]]=fa;//就要确定好父亲
son[fa][t]=son[x][t^1],son[x][t^1]=fa;//然后处理好儿子
up(fa),up(x);//注意顺序
}
int splay(int x,int &mb) {
while(x!=mb) {
int fa=f[x],g=f[fa];
if(fa!=mb) {
if(is(x)^is(fa)) spin(x,mb);
else spin(fa,mb);
}
spin(x,mb);
}
}
int find(int x,int kth) {
if(rev[x]||laz[x]) pd(x);
if(siz[son[x][0]]+1==kth) return x;
else if(siz[son[x][0]]>=kth) return find(son[x][0],kth);
else return find(son[x][1],kth-siz[son[x][0]]-1);
}
void build(int l,int r,int fa) {//扯清楚siz和伦理关系
if(l>r) return;
if(l==r) {
f[l]=fa,siz[l]=1;
if(l<fa) son[fa][0]=l;
else son[fa][1]=l;
return;
}
int mid=(l+r)>>1;
build(l,mid-1,mid),build(mid+1,r,mid);
f[mid]=fa,up(mid);
if(mid<fa) son[fa][0]=mid;
else son[fa][1]=mid;
}
int main()
{
int bj,l,r,z,kl;
mx[0]=-inf;//最大值可能为负数
n=read(),m=read();
build(1,n+2,0),rt=(n+3)>>1;//注意添加哨兵节点(1,n+2)
splay(4,rt);
for(int i=1;i<=m;++i) {
bj=read(),l=read(),r=read();
int x=find(rt,l),y=find(rt,r+2);
splay(x,rt),splay(y,son[rt][1]),kl=son[y][0];
if(bj==1) z=read(),laz[kl]+=z,v[kl]+=z,mx[kl]+=z;
if(bj==2) rev[kl]^=1;
if(bj==3) printf("%d\n",mx[kl]);
}
return 0;
}