Splay—平衡树学习笔记

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;//当然也可以按照中序遍历输出
}

洛谷P3165 [CQOI2014]排序机械臂【splay】题解

洛谷P2596 [ZJOI2006]书架【splay】题解

洛谷P4008 [NOI2003]文本编辑器【splay】题解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值