Splay 结构体
其实并没有放在结构体里
size[]表示每个节点包括自己在内的儿子数量
ch[][2] ,fa[];ch是结点的左右儿子指针,fa是节点的父指针
这些是建立splay所必需的
对于题目要求维护的信息需要自行加入数组
Splay 伸展与旋转
splay的伸展分为几种情况
假设我们要将 编号为x的结点旋转到根p,设y=fa[x],z=fa[y]
即y是x的父亲,z是x的爷爷
若y==rt,直接将x向上旋转
若y!=rt
此时若z ,y ,x 在一条直线上,则先将y向上旋转,再将x向上旋转
若三个节点不在一条直线上,则将x向上旋转两次
反复执行上述操作直到x==rt
void splay(int& p,int x) //p为要旋转到的结点,记得加引用
{
while(x!=p)
{
int y=fa[x],z=fa[y];
if(y!=p)
{
if((ch[y][0]==x)^(ch[z][0]==y)) rotate(p,x);
else rotate(p,y);
}
rotate(p,x);
}
}
然而上面splay只说旋转,到底往哪边旋呢
这就要看rotate操作了
void rotate(int& p,int x) //同样p是旋转的目标结点
{
int y=fa[x],z=fa[y];
int t=(ch[y][0]==x);
//rotate的精髓,判断旋转方向,若x是y的左儿子,则t=1,否则t=0
if(y==p) p=x; //以下都是把x旋转到y的位置的操作
else if(ch[z][0]==y) ch[z][0]=x;
else ch[z][1]=x;
fa[y]=x; fa[ch[x][t]]=y; fa[x]=z; //一系列更新操作
ch[y][t^1]=ch[x][t]; ch[x][t]=y;
update(y);update(x); //最后记得维护节点信息
}
其实对于有treap基础的同学来说应该不难理解
每次想不起来把上面图画一画就好了
Splay 建树
常见的建树方式有两种
这里均以插入一个1-n的序列为例
解释前首先先明确在splay tree维护的序列中
每个节点的左子树元素在当前序列中一定在该元素前面
而右子树元素则一定在后面
ins直接插入+splay伸展
void ins(int x)
{
v[++sz]=x; pos[x]=sz;//直接给每一次插入的元素按1-n顺序指定编号
size[sz]=1; ch[sz][0]=ch[sz][1]=0;//初始化
if (sz>1)
{
//因为是顺序插入,所以直接指定为上一个编号的右儿子
ch[sz-1][1]=sz;fa[sz]=sz-1;
splay(rt,sz);//将当前结点伸展到根,以确保每次插入时间复杂度
}
}
for(int i=1;i<=n;++i){int x=read();ins(x);}//读入序列元素并插入
这种建树方式较易理解,但平均复杂度可能较高
其实也不会差太多,卡不掉的
二分插入
有些类似线段树的建树
先制定rt=1+n>>1
然后调用build(rt,1,n);
void build(int p,int ll,int rr)
{
if(ll>rr) return;
int mid=ll+rr>>1;
fa[mid]=p; size[mid]=1;
ch[p][mid>p]=mid;//初始化该节点信息
if(ll==rr) return;//叶子节点
build(mid,ll,mid-1);build(mid,mid+1,rr);
//二分建树,注意左区间是ll-mid-1,和线段树不同
update(mid); //维护信息
}
整体复杂度较低,个人比较喜欢这种
Splay 查询
和treap查询差不多
主要用处是在splay要更新ll-rr区间时
找到第ll和第rr个元素是什么
int find(int p,int k) //查询splay tree中第k个元素
{
int ss=size[ch[p][0]];
if(k==ss+1) return p;
if(k<=ss) return find(ch[p][0],k);
else return find(ch[p][1],k-ss-1);
}
Splay 更新
void update(int p)
{
size[p]=size[ch[p][0]]+size[ch[p][1]]+1;
//可自行添加其他...
}
这个没什么好讲的
主要是记得在那些地方要调用
splay应用
P3391 【模板】文艺平衡树(Splay)
借用线段树思想
用lzy[u]=1表示以u为根的子树需要反转
然而问题是怎么找到需要反转的区间呢
假设我们要得到 ll-rr 区间
我们先找到当前序列第ll-1个元素(设为x)和第rr+1个元素(设为y)
将x旋转到rt,再将y旋转到ch[rt][1]
这时ch[y][0],也就是根的右儿子的左子树,就是我们要的区间
仔细思考,是不是很神奇
注意因为要旋转ll-1与rr+1
所以序列需要加两个哨兵结点1与n+2
而原序列就变成了2-n+1
记得每次输入的操作区间要先加1
到这里我们只要给ch[y][0]打上标记就好了
然后在需要的时候下放标记
void push(int p)
{
if(!lzy[p]) return;
swap(ch[p][0],ch[p][1]);//交换左右子树
lzy[ch[p][1]]^=1; lzy[ch[p][0]]^=1;//下放标记
lzy[p]=0;
}
完整代码
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
int read()
{
int x=0,f=1;
char ss=getchar();
while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
return f*x;
}
int n,m;
int rt;
int size[100010],fa[100010],ch[100010][2];
int lzy[100010];
void update(int p)
{
size[p]=size[ch[p][0]]+size[ch[p][1]]+1;
}
void push(int p)
{
if(!lzy[p]) return;
swap(ch[p][0],ch[p][1]);//交换左右子树
lzy[ch[p][1]]^=1; lzy[ch[p][0]]^=1;//下放标记
lzy[p]=0;
}
void build(int p,int ll,int rr)
{
if(ll>rr) return;
int mid=ll+rr>>1;
fa[mid]=p; size[mid]=1;
ch[p][mid>p]=mid;
if(ll==rr) return;
build(mid,ll,mid-1);build(mid,mid+1,rr);
update(mid);
}
int find(int p,int k)
{
push(p);
int ss=size[ch[p][0]];
if(k==ss+1) return p;
if(k<=ss) return find(ch[p][0],k);
else return find(ch[p][1],k-ss-1);
}
void rotate(int& p,int x) {
int y=fa[x],z=fa[y];
int t=(ch[y][0]==x);
if(y==p) p=x;
else if(ch[z][0]==y) ch[z][0]=x;
else ch[z][1]=x;
fa[y]=x; fa[ch[x][t]]=y; fa[x]=z;
ch[y][t^1]=ch[x][t]; ch[x][t]=y;
update(y);update(x);
}
void splay(int& p,int x)
{
while(x!=p)
{
int y=fa[x],z=fa[y];
if(y!=p)
{
if((ch[y][0]==x)^(ch[z][0]==y)) rotate(p,x);
else rotate(p,y);
}
rotate(p,x);
}
}
void rev(int ll,int rr)
{
int x=find(rt,ll-1),y=find(rt,rr+1);//找到第ll-1和rr+1个元素编号
splay(rt,x);
splay(ch[x][1],y); //伸展
lzy[ch[y][0]]^=1;//标记
}
int main() {
n=read();m=read();
rt=n+3>>1;//因为加了两个哨兵结点,所以rt是(n+3)/2
build(rt,1,n+2);
while(m--)
{
int ll=read(),rr=read();
rev(ll+1,rr+1);//操作区间后移
}
for(int i=2;i<=n+1;++i)
printf("%d ",find(rt,i)-1);//输出相当于从1开始每次找到第k个元素
return 0;//当然也可以按照中序遍历输出
}