题目描述
您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
- 插入 x 数
- 删除 x 数(若有多个相同的数,因只删除一个)
- 查询 x 数的排名(排名定义为比当前数小的数的个数 +1+1 。若有多个相同的数,因输出最小的排名)
- 查询排名为 x 的数
- 求 x 的前驱(前驱定义为小于 x ,且最大的数)
- 求 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;
}
搞定,希望我的讲解能对你有帮助!!!