Splay 学习笔记

一:初步认识

Splay 是一种平衡树,代码量不如无旋 Treap,支持的操作无旋 Treap 大都也支持。

那为什么我们要学它呢?理性娱乐 因为有一个叫 LCT 的神奇数据结构需要用到它。

二:实现

这部分所有的可能有图片的内容都可以到 oiwiki 的 Splay 条目 找到,本文的实现大部分也都是参考这篇文章的。

一些约定

int rt;      //平衡树的根
int val[N];  //val[x]:表示节点 x 对应的权值
int cnt[N];  //cnt[x]:表示节点 x 对应权值的个数
int sz[N];   //sz[x] :表示节点 x 的子树大小
int fa[N];   //fa[x] :表示节点 x 的父节点
int ch[N][2];//ch[x] :表示节点 x 的子节点
int tot;     //有多少节点

核心思想

Splay 通过每次操作后将访问到的节点移动至根节点来保证时间复杂度。

核心操作

Rotate 旋转操作

rotate(x) 的功能就是在保持二叉搜索树的性质的前提下,通过旋转把节点 \(x\) 向上移动。

#define Get(x) (x==ch[fa[x]][1])
void push_up(int x){sz[x]=cnt[x]+sz[ch[x][0]]+sz[ch[x][1]];}
void rotate(int x){
	int y=fa[x],z=fa[y],id=Get(x);
    if(z) ch[z][Get(y)]=x;
    ch[y][id]=ch[x][id^1],ch[x][id^1]=y;
    if(ch[y][id]) fa[ch[y][id]]=y;
    fa[y]=x,fa[x]=z;
    push_up(y);
    push_up(x);
}
Splay 伸展操作

Splay(x) 通过三种操作将 \(x\) 节点移动到根节点处,并成为新的根节点。

void Splay(int &z,int x){
    int w=fa[z];
    for(int y;(y=fa[x])!=w;rotate(x))
        if(fa[y]!=w)
            rotate(Get(x)==Get(y) ? y : x);
    z=x;
}

平衡树操作

根据权值找节点

像其他平衡树一样根据二叉搜索树的性质进行搜索即可。不要忘了将访问到节点移到根节点处!!!

这个函数有一个额外功能:如果不存在这个权值,那么它会将第一个大于/小于该权值的节点移到根节点处。

void find(int &z,int v){
    int x=z,y=fa[z];
    while(x && val[x]!=v)
        y=x,x=(v<val[x] ? ch[x][0] : ch[x][1]);
    Splay(z,x ? x : y);
}
根据排名找节点

像其他平衡树一样根据二叉搜索树的性质进行搜索即可。不要忘了将访问到节点移到根节点处!!!

void find_rak(int &z,int k){
    int x=z;
    while(x){
        if(k<=sz[ch[x][0]])
            x=ch[x][0];
        else if(k<=sz[ch[x][0]]+cnt[x])
            break;
        else
            k-=sz[ch[x][0]]+cnt[x],x=ch[x][1];
    }
    Splay(z,x);
}
插入操作

先找到这个权值对应的节点,如果不存在这个节点,就新开节点;否则就维护节点信息。

void insert(int v){
    int x=rt,y=0;
    while(x && val[x]!=v)
        y=x,x=(v<val[x] ? ch[x][0] : ch[x][1]);
    if(x) ++sz[x],++cnt[x];
    else{
        x=++tot;
        sz[x]=cnt[x]=1;
        val[x]=v;
        fa[x]=y;
        if(y) ch[y][v>val[y]]=x;
    }
    Splay(rt,x);
}
合并操作

在某些操作中,我们需要合并两颗 Splay,并且保证其中一颗 \(A\) 的最小权值大于另一颗 \(B\) 的最大权值。

那么我们就把 \(A\) 中权值最小的节点移到根处,并将 \(B\) 的根节点连向 \(A\) 的根,可以证明这样合并后新的树一定满足二叉搜索树的性质。

int merge(int x,int y){
    if(!x || !y) return x|y;
    find_rak(y,1);
    ch[y][0]=x;
    fa[x]=y;
    push_up(y);
    return y;
}
删除操作

先找到权值对应节点,然后更新节点信息,如果节点已经为空节点,那么就合并它的左右子树。

void del(int v){
    find(rt,v);
    if(!rt || val[rt]!=v) return;
    --cnt[rt];
    --sz[rt];
    if(!cnt[rt]){
        int ls=ch[rt][0],rs=ch[rt][1];
        fa[ls]=fa[rs]=0;
        rt=merge(ls,rs);
    }
}
问排名

先找到权值对应节点,如果找到的节点权值大于询问权值,那么要把当前点的个数也加上(因为不一定存在这个权值)。

int getrank(int v){
    find(rt,v);
    return sz[ch[rt][0]]+(val[rt]<v ? cnt[rt] : 0) +1;
}
问前驱

先找到权值对应节点,如果找到的节点权值小于询问权值,那么答案就是这个节点的权值;否则就一直跳右儿子。

int getpre(int v){
    find(rt,v);
    if(rt && val[rt]<v) return val[rt];
    int x=ch[rt][0];
    if(!x) return -1;
    while(ch[x][1]) x=ch[x][1];
    Splay(rt,x);
    return val[x];
}
问后缀

与问前缀类似。

int getsuf(int v){
    find(rt,v);
    if(rt && val[rt]>v) return val[rt];
    int x=ch[rt][1];
    if(!x) return -1;
    while(ch[x][0]) x=ch[x][0];
    Splay(rt,x);
    return val[x];
}
posted @ 2025-08-24 22:23  XiaoZi_qwq  阅读(18)  评论(0)    收藏  举报