splay的各种操作与简易讲解

基本操作

插入

和二叉查找树一样,但是插入完成后要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]);
}

伸展

zig和zag
zig-zig
zig-zag

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;
}

bzoj1500 维护数列

戳我看代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值