平衡树之Splay

算法简介

Splay是一种平衡树,支持插入、删除、求排名、求第\(k\)大数、求前驱和求后继的操作,并且它还能做到一般平衡树做不到的区间操作。

定义与性质

先说二叉查找树:就是把所有数建在树上,且左边的数永远小于右边的。
对于上面说的那6个操作,其实在数据随机时二叉查找树时最强的,但是数据一条链你就Good Game了。
这种情况我们希望这棵二叉查找树的节点深度差不要太大,这就有了平衡树。
顾名思义,平衡树是平衡的二叉查找树,意思就是说1条链这种数据对于平衡树来说完全不存在,这样复杂度就有保证了。

基础操作

核心操作

这些都是很重要的操作,直接维护了Splay的平衡。

pushup

维护子树大小,很简单,不谈。好像只在旋转操作中出现。

inline void pushup(int x)
{
    siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+ct[x];
}

左右旋

核心中的核心,是另外一个核心操作的基础核心操作。
这个东西可以改变两个相邻节点的父子关系,并且仍然满足平衡树的性质。给张图,就可以看明白了。

inline void rota(int x)
{
    int y=ff[x],z=ff[y],k=(x==ch[y][1]);
    ch[z][y==ch[z][1]]=x;
    ff[x]=z;
    ch[y][k]=ch[x][k^1];
    ff[ch[x][k^1]]=y;
    ch[x][k^1]=y;
    ff[y]=x;
    pushup(x),pushup(y);
}

splay

为什么Splay要叫Splay?因为这个操作。这个操作就是直接保证了Splay的复杂度。
这个操作就是把一个节点旋转为目标节点的子节点,或者旋转到根节点。
这里情况有点小多(写成代码就短了,毕竟这个代码经过了一代又一代压行先辈的优化):
若该节点的爷爷节点为目标节点,直接把这个节点旋上去。
若爷爷节点不为目标节点,且该节点、父节点、爷爷节点三点一线,那么先旋父节点,再旋该节点。
若爷爷节点不为目标节点,且该节点、父节点、爷爷节点三点不共线,那么旋2次该节点。

inline void splay(int x,int father)
{
    while(ff[x]!=father)
    {
        int y=ff[x],z=ff[y];
        if(z!=father)
          (y==ch[z][0])^(x==ch[y][0])?rota(x):rota(y);//压行神器
        rota(x);
    }
    if(!father)
      root=x;
}

6个操作

其实就跟二叉查找树差不多。

插入

我们给每个节点设计数器,表示这个节点代表的数有多少个。
先从根节点开始找这个数,每次都要记录目前节点及其父亲。
找到了(最后记录目前节点的变量不为\(0\))就说明这个数存在,存在就把该节点计数器\(+1\)。
没找到,那么我们可以肯定刚才的父亲可以作为新数的父亲,于是新数就变成它儿子了。注意,若父亲为\(0\),则新数为根。
最后要把新数旋为根节点,保证随机性。

inline void Insert(int x)
{
    int now=root,father=0;
    while(now&&x!=val[now])
    {
        father=now;
        now=ch[now][x>val[now]];
    }
    if(now)
      ct[now]++;
    else
    {
        now=++cnt;
        if(!father)
          root=now;
        else ch[father][x>val[father]]=now;
        ff[now]=father,val[now]=x,ct[now]=1,siz[now]=1;
    }
    splay(now,0);
}

删除

先找前驱、后继的节点编号(下面会讲,可以先看下面),然后把前驱旋为根节点,后继旋为根节点的右儿子,那么此时待删除节点就被卡在后继的左儿子那儿了。
之后还是看看计数器,如果大于\(1\),那么就直接\(-1\)之后旋转该点到根节点保证随机,否则干净利落地砍掉。
为了防止找不到前驱后继,我们一开始就插入\(-\inf\)以及\(\inf\)。

void Delete(int x)
{
    int xp=qpre(x),xs=qsuf(x);
    splay(xp,0),splay(xs,xp);
    int now=ch[xs][0];
    if(ct[now]>1)
    {
        ct[now]--;
        splay(now,0);
    }
    else ch[xs][0]=0;
}

查询第\(k\)大数

从根节点节点开始走,如果该节点的左子树的大小大于\(k\),那么继续往左子树走。
否则如果该节点左子树的大小加上该点计数器小于\(k\),答案就是该节点的值。
否则往左边走,同时\(k\)要减去该节点计数器以及该节点左子树大小。

inline int kth(int x)
{
    int now=root;
    if(siz[now]<x)
      return 0;
    while(1)
    {
        if(siz[ch[now][0]]>=x)
          now=ch[now][0];
        else if(siz[ch[now][0]]+ct[now]>=x)
          return val[now];
        else x-=siz[ch[now][0]]+ct[now],now=ch[now][1];
    }
}

查询排名

这个直接先找到该数所在节点,然后答案就是左子树大小\(+1\)。
记得最后还要\(-1\),因为你插入了\(-inf\)。

inline int qrank(int x)
{
    findx(x);
    return siz[ch[root][0]]+1;
}

前驱

找到该数节点,然后从左儿子开始一直走其右儿子,最后到的就是前驱。
为了避免找不到的问题,先插入,再删除。

inline int qpre(int x)
{
    findx(x);
    int now=ch[root][0];
    while(ch[now][1])
      now=ch[now][1];
    return now;
}

后继

同理,不多扯了。

inline int qsuf(int x)
{
    findx(x);
    int now=ch[root][1];
    while(ch[now][0])
      now=ch[now][0];
    return now;
}

它活着的意义?

我们发现,这个东西常数贼大,还长。显然,我们面对上面6种操作,用treap不行吗?
对,是。但是根据黑格尔的存在即合理理论,dzy大佬花很多时间把这个东西教给我们,教练让我花很多时间把这个东西学会,我花很多时间把这个东西学会了并写出来,评测机花很多时间去评测,那这个东西肯定在别的方面更加突出。
是的,Splay可以处理区间问题(常常是连线段树也解决不了的),而别的平衡树面对区间问题捉襟见肘。
我们处理区间问题,首先直接递归建树,建树时,节点间的大小关系不再是储存的值的关系,而是节点序号的关系。这里我们就要把节点编号设为节点在原序列中的位置。
于是我们发现,某子树的中序遍历,就为一个区间(有时可以反过来化树为区间,多用于DP)。这时每个节点就可以代表该子树的一段区间了。你还可以在节点上打lazy标记优化时间复杂度。
但是说到这里,我们发现几乎所有平衡树都有这个性质啊!那么为什么一般的平衡树很难处理区间问题?
问题在于,你很难从平衡树上找到这个区间。像treap,你怎么找?
但是Splay能找。对于区间\(l~r\),只需要把\(l-1\)旋到根节点,把\(r+1\)旋到根节点的右儿子,那么根节点左儿子就是要的区间。这样一个区间就被提取出来了。
事实上,一个平衡树能否能处理区间问题,就要看能否快速提取区间。于是,我们发现好像只有Splay和FHQ无旋treap(常数更小)可以做到。于是好像只有它们可以同时做到区间问题和6种操作。
事实上,还有一种数据结构也可以,那就是树状数组,不过它是主要处理区间问题,由于某些原因可以搞6种操作,在此就不提了(其实是我不会树状数组处理6种操作)。

应用

Splay可以用于LCT。尽管Splay常数比FHQ大,但是在LCT里面,不知道为什么它复杂度更小,所以用它。

代码

1、普通平衡树(6种操作)

#include<bits/stdc++.h>
using namespace std;
int n,q,root,cnt,a[100001],ff[100001],ch[100001][2],val[100001],siz[100001],ct[100001];

inline int Read()
{
    int x=0,f=1;
    char cha=getchar();
    while(!isdigit(cha))
    {
        if(cha=='-')
          f=-1;
        cha=getchar();
    }
    while(isdigit(cha))
    {
        x=(x<<3)+(x<<1)+cha-'0';
        cha=getchar();
    }
    return x*f;
}
inline void pushup(int x)
{
    siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+ct[x];
}
inline void rota(int x)
{
    int y=ff[x],z=ff[y],k=(x==ch[y][1]);
    ch[z][y==ch[z][1]]=x;
    ff[x]=z;
    ch[y][k]=ch[x][k^1];
    ff[ch[x][k^1]]=y;
    ch[x][k^1]=y;
    ff[y]=x;
    pushup(x),pushup(y);
}
inline void splay(int x,int father)
{
    while(ff[x]!=father)
    {
        int y=ff[x],z=ff[y];
        if(z!=father)
          (y==ch[z][0])^(x==ch[y][0])?rota(x):rota(y);
        rota(x);
    }
    if(!father)
      root=x;
}
inline void Insert(int x)
{
    int now=root,father=0;
    while(now&&x!=val[now])
    {
        father=now;
        now=ch[now][x>val[now]];
    }
    if(now)
      ct[now]++;
    else
    {
        now=++cnt;
        if(!father)
          root=now;
        else ch[father][x>val[father]]=now;
        ff[now]=father,val[now]=x,ct[now]=1,siz[now]=1;
    }
    splay(now,0);
}
inline void findx(int x)
{
    int now=root;
    if(!now)
      return;
    while(ch[now][x>val[now]]&&val[now]!=x)
      now=ch[now][x>val[now]];
    splay(now,0);
}
inline int qpre(int x)
{
    findx(x);
    int now=ch[root][0];
    while(ch[now][1])
      now=ch[now][1];
    return now;
}
inline int qsuf(int x)
{
    findx(x);
    int now=ch[root][1];
    while(ch[now][0])
      now=ch[now][0];
    return now;
}
void Delete(int x)
{
    int xp=qpre(x),xs=qsuf(x);
    splay(xp,0),splay(xs,xp);
    int now=ch[xs][0];
    if(ct[now]>1)
    {
        ct[now]--;
        splay(now,0);
    }
    else ch[xs][0]=0;
}
inline int kth(int x)
{
    int now=root;
    if(siz[now]<x)
      return 0;
    while(1)
    {
        if(siz[ch[now][0]]>=x)
          now=ch[now][0];
        else if(siz[ch[now][0]]+ct[now]>=x)
          return val[now];
        else x-=siz[ch[now][0]]+ct[now],now=ch[now][1];
    }
}
inline int qrank(int x)
{
    findx(x);
    return siz[ch[root][0]]+1;
}
int main()
{
    q=Read();
    int opt,k;
    Insert(2147483647),Insert(-2147483647);
    while(q--)
    {
        opt=Read(),k=Read();
        if(opt==1)
        {
            Insert(k);
        }
        else if(opt==2)
        {
            Delete(k);
        }
        else if(opt==3)
        {
            printf("%d\n",qrank(k)-1);
        }
        else if(opt==4)
        {
            printf("%d\n",kth(k+1));
        }
        else if(opt==5)
        {
            Insert(k);
            printf("%d\n",val[qpre(k)]);
            Delete(k);
        }
        else
        {
            Insert(k);
            printf("%d\n",val[qsuf(k)]);
            Delete(k);
        }
    }
    return 0;
}

2、文艺平衡树(区间翻转)
还没写,先鸽着。

原文地址:https://www.cnblogs.com/SKTT1Faker/p/12075275.html

时间: 2024-10-16 02:29:32

平衡树之Splay的相关文章

P3391 【模板】文艺平衡树(Splay)新板子

P3391 [模板]文艺平衡树(Splay) 题目背景 这是一道经典的Splay模板题——文艺平衡树. 题目描述 您需要写一种数据结构(可参考题目标题),来维护一个有序数列,其中需要提供以下操作:翻转一个区间,例如原有序序列是5 4 3 2 1,翻转区间是[2,4]的话,结果是5 2 3 4 1 输入输出格式 输入格式: 第一行为n,m n表示初始序列有n个数,这个序列依次是(1,2, \cdots n-1,n)(1,2,?n−1,n) m表示翻转操作次数 接下来m行每行两个数 [l,r][l,

【BZOJ】3223: Tyvj 1729 文艺平衡树(splay)

http://www.lydsy.com/JudgeOnline/problem.php?id=3223 默默的.. #include <cstdio> #include <cstring> #include <cmath> #include <string> #include <iostream> #include <algorithm> #include <queue> #include <set> #in

P3391 【模板】文艺平衡树(Splay)

题目背景 这是一道经典的Splay模板题--文艺平衡树. 题目描述 您需要写一种数据结构(可参考题目标题),来维护一个有序数列,其中需要提供以下操作:翻转一个区间,例如原有序序列是5 4 3 2 1,翻转区间是[2,4]的话,结果是5 2 3 4 1 输入输出格式 输入格式: 第一行为n,m n表示初始序列有n个数,这个序列依次是(1,2, \cdots n-1,n)(1,2,?n?1,n) m表示翻转操作次数 接下来m行每行两个数 [l,r][l,r] 数据保证 1 \leq l \leq r

平衡树(splay)

求区间最大值模板 1 struct node{ 2 int f[maxn];//父亲结点 3 int ch[maxn][2];//左右孩子 4 int key[maxn];//结点i代表的数字 5 int cnt[maxn];//结点i出现的次数,也可以为全值.平衡树没有相同值的结点,所以如果出现了相同值时,cnt[i]++,或者cnt[i] += key[i] 6 int siz[maxn];//包括i的子树的大小 7 int val[maxn]; 8 int Max[maxn]; 9 int

[知识点]平衡树之Splay

// 此博文为迁移而来,写于2015年7月18日,不代表本人现在的观点与看法.原始地址:http://blog.sina.com.cn/s/blog_6022c4720102w6rg.html 1.前言 这玩意儿真的搞了我好久,当然前一阵子一直都没有去管它,最近直接参照了YML(@YMDragon)的程序,感觉还是不错的,大概看得懂了,就是逻辑关系有点扯.好,废话不多说…… 2.概念 平衡树的全称为:平衡二叉搜索树,功能很强大,带来的后果就是代码非常复杂.首先大家应该都知道二叉搜索树是什么了,那

洛谷P3391 【模板】文艺平衡树(Splay)

题目背景 这是一道经典的Splay模板题——文艺平衡树. 题目描述 您需要写一种数据结构(可参考题目标题),来维护一个有序数列,其中需要提供以下操作:翻转一个区间,例如原有序序列是5 4 3 2 1,翻转区间是[2,4]的话,结果是5 2 3 4 1 输入输出格式 输入格式: 第一行为n,m n表示初始序列有n个数,这个序列依次是(1,2, \cdots n-1,n)(1,2,?n−1,n) m表示翻转操作次数 接下来m行每行两个数 [l,r][l,r] 数据保证 1 \leq l \leq r

洛谷P3391 【模板】文艺平衡树(Splay)(FHQ Treap)

题目背景 这是一道经典的Splay模板题——文艺平衡树. 题目描述 您需要写一种数据结构(可参考题目标题),来维护一个有序数列,其中需要提供以下操作:翻转一个区间,例如原有序序列是5 4 3 2 1,翻转区间是[2,4]的话,结果是5 2 3 4 1 输入输出格式 输入格式: 第一行为n,m n表示初始序列有n个数,这个序列依次是(1,2, \cdots n-1,n)(1,2,?n−1,n) m表示翻转操作次数 接下来m行每行两个数 [l,r][l,r] 数据保证 1 \leq l \leq r

【模板】文艺平衡树(Splay)

题目背景 这是一道经典的Splay模板题--文艺平衡树. 题目描述 您需要写一种数据结构(可参考题目标题),来维护一个有序数列,其中需要提供以下操作:翻转一个区间,例如原有序序列是5 4 3 2 1,翻转区间是[2,4]的话,结果是5 2 3 4 1 输入输出格式 输入格式: 第一行为n,m n表示初始序列有n个数,这个序列依次是(1,2, \cdots n-1,n)(1,2,?n?1,n) m表示翻转操作次数 接下来m行每行两个数 [l,r][l,r] 数据保证 1 \leq l \leq r

文艺平衡树(splay模板)

题干:splay模板,要求维护区间反转. splay是一种码量小于treap,但支持排名,前驱后继等treap可求的东西,也支持区间反转的平衡树. 但是有两个坏处: 1.splay常数远远大于treap以及stl中的set. 2.没有可持久化splay,但有可持久化treap. 下面是代码: 1.pushup以及pushdown pushup用于维护某点所在子树大小. void pushup(int u) { tr[u].siz = tr[tr[u].ch[0]].siz + tr[tr[u].