splay【模板】

题目描述

您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:

  1. 插入 x 数
  2. 删除 x 数(若有多个相同的数,因只删除一个)
  3. 查询 x 数的排名(排名定义为比当前数小的数的个数 +1+1 。若有多个相同的数,因输出最小的排名)
  4. 查询排名为 x 的数
  5. 求 x 的前驱(前驱定义为小于 x ,且最大的数)
  6. 求 x 的后继(后继定义为大于 x ,且最小的数)

输入输出格式

输入格式:

第一行为 n,表示操作的个数,下面 n 行每行有两个数 opt 和 x , optt 表示操作的序号( 1≤opt≤6 

输出格式:

对于操作 3,4,5,6 每行输出一个数,表示对应答案。

 

 

好吧,我承认我是从洛谷P3369上抄的题目,不过get了splay这个新技能我非常开心(尽管我并没有弄明白区间翻转),希望我也能把这个技能分享给大家。

先解释一下各个数组的含义:(N代表一共要开几个点,视情况改变)

1.son[N][2]

顾名思义,son[x][0]——x的左儿子,son[x][1]——x的右儿子。

因为splay是一棵搜索树,所以满足x的左儿子的权值比x的权值小,x的右儿子的权值比x的权值大。

2.f[N]

同上,f[x]就代表在这颗搜索树中x的父节点。

3.key[N]

key[x]代表x节点的权值。

4.cou[N]

因为是二叉搜索树,所以不能出现权值相同的点,所以cou[x]代表x节点的权值出现的次数。

5.siz[N]

siz[x]代表以x为根节点的子树大小(包括其本身)。

6.root

就是当前树的根节点(因为splay会不断旋转,所以根也不定)。

7.sz

动态开点的指针(不懂动态开点我也没办法……)。

 

好,那接下来就从splay中一个个函数说起吧。

1.clear()

没啥太大用处,就是在删除函数中用来清除某个点的。

代码:

void clear(int x) {
	son[x][0]=son[x][1]=cou[x]=f[x]=siz[x]=key[x]=0;
}

2.get()

就是判断x是它父节点的左儿子还是右儿子。

代码:

bool get(int x) {
	return son[f[x]][1]==x;//是就返回1,否则返回0.
}

3.update()

在这道题里也就是更改一下siz[x],不同的题里不同,视题而定。

代码:

void update(int x) {
	if(x) {//如果x不是虚拟父节点
		siz[x]=cou[x];
		if(son[x][0])siz[x]+=siz[son[x][0]];
		if(son[x][1])siz[x]+=siz[son[x][1]];
	}
}

4.rotate()

这才是splay的核心!!

因为普通的二叉搜索树很容易被卡,比如对于2、15、42、63、75、89、97,

如果建成这样

是最理想的,时间复杂度刚好O(log(n));

但是如果……

那就跟暴力搜索没啥区别了,时间复杂度很容易被卡成O(n).

所以,我们就需要在建树的过程中不断的改变根节点,才能保证时间复杂度(但是要求严格的证明我可不会,你们还是找其他大佬吧)。

然后怎么旋转呢?

(正常应该是有左旋和右旋的,然而我懒,就写成一个函数了……)

举个例子(就拿右旋来说吧)

先画个图:

现在是A < y < B < z < C < x

然后呢我们希望把z旋转到y处,怎么办呢?

首先我们先让B变成y的右儿子,B的父亲变成y,

然后我们在讲y变成z的左儿子,y的父亲变成z,

最后我们将x的左儿子改为z,z的父亲改为x(前提是x不是虚拟父节点)。

改一下,就是

现在我们再来看,大小关系还是满足A < y < B < z < C < x.

非常完美,然后呢我们就可以随便改变节点的位置,同时还不改变大小关系。

(左旋同理)

这样我们就可以避免被卡成链的情况

代码:

void rotate(int x) {
	int old=f[x],oldf=f[old],whichson=get(x);
	son[old][whichson]=son[x][whichson^1],f[son[old][whichson]]=old;
	son[x][whichson^1]=old,f[old]=x;
	f[x]=oldf;
	if(oldf) {
		son[oldf][son[oldf][1]==old]=x;
	}
	update(old);//这时old为x的儿子,所以先update(old)。
	update(x);
}

(不太懂的可以自己模拟一下,毕竟手画那么多图很累……)

5.splay()

这个函数就是旋转节点的。(注意如果x,f[x],f[f[x]]在一条链上,要先rotate(f[x]),然后在rotate(x),据说只rotate(x)会被卡)。

代码:

void splay(int x,int goal) {//将x旋转为goal的儿子
	for(int fa; (fa=f[x])!=goal; rotate(x)) {
		if(f[fa]!=goal) {
			rotate((get(x)==get(fa))?fa:x);
		}
	}
	if(!goal)root=x;
}

(其实这道题可以偷个懒,我们可以直接将x旋转成根节点,这样有利于下面做题)

偷懒代码:

void splay(int x) {
	for(int fa; (fa=f[x]); rotate(x)) {
		if(f[fa]) {
			rotate((get(x)==get(fa))?fa:x);
		}
	}
	root=x;
}

6.insert()

加入节点。

1.如果root=0,说明树为空,特判加入即可。

2.如果不为空,则:

          (1).如果存在于x权值相同的节点,那么cou[节点标号]++,再用splay维护一下siz

          (2).如果不存在,那么新开一个节点,再用splay维护一下siz

代码:

void insert(int x) {
	if(root==0) {
		sz++;
		son[sz][0]=son[sz][1]=f[sz]=0;
		key[sz]=x;
		cou[sz]=siz[sz]=1;
		root=sz;
		return;
	}
	int now=root,fa=0;
	while(1) {
		if(x==key[now]) {
			cou[now]++;
			update(now);
			update(fa);
			splay(now);
			break;
		}
		fa=now;
		now=son[now][key[now]<x];//判断x在now的左儿子里还是右儿子里
		if(now==0) {
			sz++;
			f[sz]=fa;
			son[sz][0]=son[sz][1]=0;
			cou[sz]=siz[sz]=1;
			son[fa][key[fa]<x]=sz;
			key[sz]=x;
			update(fa);
			splay(sz);
			break;
		}
	}
}

7.find()

寻找x的排名

1.如果x小于key[now],就寻找now的左儿子

2.如果x等于key[now],就直接return (ans+1) 

3.如果x大于key[now],那么ans+=siz[son[now][0]]+cou[now],然后寻找now的右儿子

代码:

int find(int x) {
	int now=root,ans=0;
	while(1) {
		if(x<key[now]) {
			now=son[now][0];
		} else {
			ans+=(son[now][0]?siz[son[now][0]]:0);
			if(x==key[now]) {
				splay(now);
				return ans+1;
			}
			ans+=cou[now];
			now=son[now][1];
		}
	}
}

8.findx()

寻找第x大的元素

1.如果now左儿子的大小>x,则直接寻找now的左儿子

2.如果now左儿子的大小和now的大小之和大于等于x,则key[now]就是答案

3.如果now左儿子的大小和now的大小之和小于x,则x-=now左儿子的大小和now的大小之和,然后在now右子树里寻找第x大的元素

代码:

int findx(int x) {
	int now=root;
	while(1) {
		if(son[now][0] && x<=siz[son[now][0]]) {
			now=son[now][0];
		} else {
			int temp=(son[now][0]?siz[son[now][0]]:0)+cou[now];
			if(x<=temp)return key[now];
			x-=temp;
			now=son[now][1];
		}
	}
}

9.pre(),next()

就是找到x的前驱和后继,这时候就体现出偷懒代码的好处了!

我们先不管有没有x这个节点,先暴力加入,然后把x旋转到根节点

这个时候我们利用二叉搜索树的性质:

想一下,想在x的左儿子的右儿子的右儿子的右儿子……的值不就是x的前驱吗?

同理x的右儿子的左儿子的左儿子的左儿子……的值不就是x的后继吗?

然后我们找到了所求值,再把x删掉就好啦!

代码:

int pre() {
	int now=son[root][0];
	while(son[now][1])now=son[now][1];
	return now;
}
int next() {
	int now=son[root][1];
	while(son[now][0])now=son[now][0];
	return now;
}

10.del()

最后一个函数了!!

我们先把x旋转到根节点,然后

1.如果cou[x]>1,那就cou[x]--,在update一下就行了

2.如果现在root既没有左儿子有没有右儿子(就是只剩root一个节点了),那就clear(root)就行了

3.如果现在root没有左儿子,那就把root变成现在root的右儿子,clear(原来的root)

4.如果现在root没有右儿子,那就把root变成现在root的左儿子,clear(原来的root)

5.如果不是以上几种情况,那就先找到x的前驱,把x的前驱旋转为根节点,然后此时x一定没有左儿子,那么直接将x的右儿子变成x前驱的右儿子,再clear(x)

代码:

void del(int x) {
	int whatever=find(x);
	if(cou[root]>1) {
		cou[root]--;
		update(root);
		return;
	}
	if(!son[root][0] && !son[root][1]) {
		clear(root);
		root=0;
		return;
	}
	if(!son[root][0]) {
		int oldroot=root;
		root=son[root][1];
		f[root]=0;
		clear(oldroot);
		return;
	}
	if(!son[root][1]) {
		int oldroot=root;
		root=son[root][0];
		f[root]=0;
		clear(oldroot);
		return;
	}
	int leftbig=pre(),oldroot=root;
	splay(leftbig);
	son[root][1]=son[oldroot][1];
	f[son[root][1]]=root;
	clear(oldroot);
	update(root);
	return;
}

 

OK,所有函数就都解释完了,最后贴一份完整代码:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<vector>
#include<map>
#include<algorithm>
#include<queue>
#define N 1000000
#define mes(a,b) memset(a,b,sizeof(a))
using namespace std;
int son[N][2],f[N],cou[N],siz[N],key[N];
int root,sz;
void clear(int x) {
	son[x][0]=son[x][1]=cou[x]=f[x]=siz[x]=key[x]=0;
}
bool get(int x) {
	return son[f[x]][1]==x;
}
void update(int x) {
	if(x) {
		siz[x]=cou[x];
		if(son[x][0])siz[x]+=siz[son[x][0]];
		if(son[x][1])siz[x]+=siz[son[x][1]];
	}
}
void rotate(int x) {
	int old=f[x],oldf=f[old],whichson=get(x);
	son[old][whichson]=son[x][whichson^1],f[son[old][whichson]]=old;
	son[x][whichson^1]=old,f[old]=x;
	f[x]=oldf;
	if(oldf) {
		son[oldf][son[oldf][1]==old]=x;
	}
	update(old);
	update(x);
}
void splay(int x) {
	for(int fa; (fa=f[x]); rotate(x)) {
		if(f[fa]) {
			rotate((get(x)==get(fa))?fa:x);
		}
	}
	root=x;
}
void insert(int x) {
	if(root==0) {
		sz++;
		son[sz][0]=son[sz][1]=f[sz]=0;
		key[sz]=x;
		cou[sz]=siz[sz]=1;
		root=sz;
		return;
	}
	int now=root,fa=0;
	while(1) {
		if(x==key[now]) {
			cou[now]++;
			update(now);
			update(fa);
			splay(now);
			break;
		}
		fa=now;
		now=son[now][key[now]<x];
		if(now==0) {
			sz++;
			f[sz]=fa;
			son[sz][0]=son[sz][1]=0;
			cou[sz]=siz[sz]=1;
			son[fa][key[fa]<x]=sz;
			key[sz]=x;
			update(fa);
			splay(sz);
			break;
		}
	}
}
int find(int x) {
	int now=root,ans=0;
	while(1) {
		if(x<key[now]) {
			now=son[now][0];
		} else {
			ans+=(son[now][0]?siz[son[now][0]]:0);
			if(x==key[now]) {
				splay(now);
				return ans+1;
			}
			ans+=cou[now];
			now=son[now][1];
		}
	}
}
int findx(int x) {
	int now=root;
	while(1) {
		if(son[now][0] && x<=siz[son[now][0]]) {
			now=son[now][0];
		} else {
			int temp=(son[now][0]?siz[son[now][0]]:0)+cou[now];
			if(x<=temp)return key[now];
			x-=temp;
			now=son[now][1];
		}
	}
}
int pre() {
	int now=son[root][0];
	while(son[now][1])now=son[now][1];
	return now;
}
int next() {
	int now=son[root][1];
	while(son[now][0])now=son[now][0];
	return now;
}
void del(int x) {
	int whatever=find(x);
	if(cou[root]>1) {
		cou[root]--;
		update(root);
		return;
	}
	if(!son[root][0] && !son[root][1]) {
		clear(root);
		root=0;
		return;
	}
	if(!son[root][0]) {
		int oldroot=root;
		root=son[root][1];
		f[root]=0;
		clear(oldroot);
		return;
	}
	if(!son[root][1]) {
		int oldroot=root;
		root=son[root][0];
		f[root]=0;
		clear(oldroot);
		return;
	}
	int leftbig=pre(),oldroot=root;
	splay(leftbig);
	son[root][1]=son[oldroot][1];
	f[son[root][1]]=root;
	clear(oldroot);
	update(root);
	return;
}
int main() {
//freopen("add.in","r",stdin);
//freopen("add.out","w",stdout);
	int n;
	scanf("%d",&n);
	int op,x;
	for(int i=1;i<=n;i++){
		scanf("%d%d",&op,&x);
		switch(op){
			case 1:insert(x);break;
			case 2:del(x);break;
			case 3:printf("%d\n",find(x));break;
			case 4:printf("%d\n",findx(x));break;
			case 5:insert(x);printf("%d\n",key[pre()]);del(x);break;
			case 6:insert(x);printf("%d\n",key[next()]);del(x);break;
		}
	}
	return 0;
}

搞定,希望我的讲解能对你有帮助!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值