UOJ Logo zhengjiarui的博客

博客

splay (伸展树) 学习笔记

2019-04-13 10:07:54 By zhengjiarui

splay简介

splay是一颗bst,有“序列之王”的美誉,可以处理序列的一切问题。处理区间只有splay和fhq_treap两棵平衡树(当然,如果你想写,B+)可以解决。

优点:万能、常数较小、可用于link cut tree

缺点:代码长、不能可持久化。

相关文章fhq_treap link cut tree

变量声明

对于一颗合格的splay,我们需要:

int rt, tot, num[N], ch[N][2], fa[N], s[N], v[N], tag[N];

这个程序基于序列操作,要求的是区间翻转。

rt:树根

tot:目前可用的是哪个位置(和主席树回收空间的方法差不对)

num:初始序列

ch:儿子编号

fa:父亲编号

s:该节点的子树大小

v:splay数组

tag:懒标记

来一波我的鬼畜define

#define get(x) (ch[fa[x]][1]==x)
#define ls ch[x][0]
#define rs ch[x][1]

get(x)的意思是x是否为其父的右子树。

splay操作

void splay(x,goal)

我们假设有一棵原树,长成这样:

splay树满足如下的性质:中序遍历是有序的(当处理普通平衡树问题的时候,且合法的操作都符合特性)。

注意:上述提醒对于序列操作无效,只是当你推导有困难的时候自己画图(脑补)考虑一下。

通过将节点旋转至根节点来进一步操作(称为splay操作)

那么我们先来看splay操作:

第一种情况、需旋转的节点x的父亲是目标节点p。那么只要一次旋转。如图x是左儿子,需要右旋。图为将a旋转到b。图片来自百度

第二种情况、x、x的父亲、x的父亲的父亲“三点共线”(如果不能理解就看下面的图),执行两次相同方向的旋转。那么先旋转x的父节点,再旋转x。图为将x点旋转到z点。图片来自百度。

第三种情况、else。执行两次不同方向的旋转。图为将x旋转到g。

上述三种情况介绍完之后,相信同学们对于旋转(还不懂?往下翻)以及splay操作有一定的认识。

递归写法:(来源:算法竞赛入门经典)

void splay(Node* &o,int k) {
    int d=o->cmp(k);
    if (d==1) k-=o->ch[0]->s+1;
    if (d!=-1) {
        Node *p=o->ch[d];
        int d2=p->cmp(k);
        int k2=(d2==0>k:k-p->ch[0]->s-1);
        if (d2!=-1) {
            splay(p->ch[d2],k2);
            if (d==d2) rotate(o,d^1); else rotate(o->ch[d],d);
        }
    }
    rotate(o,d^1);
}

非递归写法(建议同学们背一下):

void splay(int x,int goal=0) {
    for (int f;(f=fa[x])!=goal;rotate(x))
        if (fa[f]!=goal) rotate((get(x)==get(f))?f:x);
    if (!goal) rt=x;
}

void rotate(x)

贴一张一次旋转的图。

就是上述过程中的旋转。需要完成下面4件事情:

X变到原来Y的位置 Y变成了 X原来在Y的 相对的那个儿子 Y的非X的儿子不变 X的 X原来在Y的 那个儿子不变 X的 X原来在Y的 相对的 那个儿子 变成了 Y原来是X的那个儿子

上代码。

bool get(int x) {return ch[fa[x]][1]==x;}

void rotate(int x) {
    int y=fa[x],z=fa[y],k=get(x); 
    //y是x的父亲,z是父亲的父亲,k是x是否是它父亲的右子树。 
    pushdown(y); pushdown(x); //不要忘记pushdown 
    ch[y][k]=ch[x][k^1]; fa[ch[y][k]]=y;
    ch[x][k^1]=y; fa[y]=x;
    fa[x]=z;
    if (z) ch[z][ch[z][1]==y]=x;
    pushup(y); pushup(x); //pushup 
}

int query_kth(x)

这个操作是名次树中的求名次。主要就是沿着树找下去,找到位置返回即可。

如果x比当前结点小,即应该向左子树寻找,ans不用改变。一直往左子树走,中序遍历最小

如果x比当前结点大,即应该向右子树寻找,ans需要加上左子树的大小以及根的大小。 易证

如果同学们仍然不能理解,我贴一张图:

好。明白了吧。

inline int kth(int x) {
    int pos=rt;
    while (pos) {
        pushdown(pos);
        if (x<=s[ch[pos][0]]) pos=ch[pos][0];
        else {
            x-=s[ch[pos][0]]+1;
            if (!x) return pos; //此时x和树中的点重合,树中不允许有两个相同的点
            pos=ch[pos][1]; //到达右孩子处
        }
    }
}

void build(f, l, r)

因为splay在splay的过程中会变得平衡,所以build可以随便建。可以参考线段树的build。时间复杂度$O(N)$

inline int build(int l,int r,int f) {
    if (l>r) return 0;
    int x=++tot, mid=(l+r)/2;
    fa[x]=f, v[x]=mx[x]=num[mid];
    ch[x][0]=build(l,mid-1,x);
    ch[x][1]=build(mid+1,r,x);
    pushup(x); return x;
}

void insert(x)

和普通的平衡树insert一样。简单说就是找到位置开点。想不懂的看一看querykth的那张图。

inline void insert(int x) {
    int u=root, ff=0;
    while (u && t[u].val!=x) {
        ff=u;
        u=t[u].ch[x>t[u].val];
    }
    if (u) t[u].cnt++;
    else {
        u=++tot;
        if (ff) t[ff].ch[x>t[ff].val]=u;
        t[u].ch[0]=t[u].ch[1]=0;
        t[tot].ff=ff;
        t[tot].val=x;
        t[tot].cnt=1;
        t[tot].size=1;
    }
    splay(u, 0);
}

区间操作

这里的区间操作是把l转到根节点,r转到l节点,那么所求的区间就在根的右子树的左子树。

(真想配个图,但是我画不出来……有同学画出来图联系我吧

void turn(int l,int r)
{
    l=query_kth(l); r=query_kth(r+2);
    //先让l占下根的位置,然后让r+2把他挤到左子树上去
    splay(l,0); splay(r,l);
    pushdown(rt);
    tag[ch[ch[rt][1]][0]]^=1;//根的右子树的左子树
}

pushup、pushdown

只要记住要在修改、访问之前加pushdown,在修改之后加pushup。

下面是区间翻转操作的代码。

void pushup(int x) { siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+1; }
void pushdown(int x)
{
    if (x && tag[x]) {
        tag[ch[x][0]]^=1;
        tag[ch[x][1]]^=1;
        swap(ch[x][0],ch[x][1]);
        tag[x]=0;
    }
}

split merge

不知道同学们有没有学过fhq_treap。我们都知道,fhq_treap是通过split和merge两个操作打天下的。

split指的是将某个节点的子树与其父断开,而merge指的是把断开后的连上去。

如图。

上代码:

//合并操作:把序列s1, s2连接在一起,s1在左边
Node* merge(Node* left, Node* right) {
    splay(left, left->s);
    left->ch[1] = right;
    left->maintain();
    return left;
}

//分裂操作:把序列S分成两个连续的子序列,左子序列包含k个元素,右子序列包含剩下的元素
void split(Node* o, int k, Node* &left, Node* &right) {
    splay(o, k);
    left = o;
    right = o->ch[1];
    o->ch[1] = NULL;
    left->maintain();
}

模板题

模板题应该不需要我加解析吧……

例1:luogu3391文艺平衡树

这代码不是我写的……感谢热心学长……

#include<bits/stdc++.h>
using namespace std;

const int INF=1e9;
int n,m,i,j,t,k,s,rt,tot,a[100005],ch[100005][2],fa[100005],key[100005],cnt[100005],siz[100005],tag[100005];

void pushup(int x) { siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+1; }
void pushdown(int x) {
    if (x && tag[x]) {
        tag[ch[x][0]]^=1;
        tag[ch[x][1]]^=1;
        swap(ch[x][0],ch[x][1]);
        tag[x]=0;
    }
}

bool get(int x) {return ch[fa[x]][1]==x;}

void rotate(int x) {
    int y=fa[x],z=fa[y],k=get(x); 
    //y是x的父亲,z是父亲的父亲,k是x是否是它父亲的右子树。 
    pushdown(y); pushdown(x); //不要忘记pushdown 
    ch[y][k]=ch[x][k^1]; fa[ch[y][k]]=y;
    ch[x][k^1]=y; fa[y]=x;
    fa[x]=z;
    if (z) ch[z][ch[z][1]==y]=x;
    pushup(y); pushup(x); //pushup 
}

void splay(int x,int goal=0) {
    for (int f; (f=fa[x])!=goal; rotate(x))
        if (fa[f]!=goal)
            rotate((get(x)==get(f))?f:x);
    if (!goal) rt=x;
}

int query_kth(int x) {
    int now=rt;
    while (1) {
        pushdown(now);
        if (x<=siz[ch[now][0]]) now=ch[now][0];
        else {
            x-=siz[ch[now][0]]+1;
            if (!x) return now;
            now=ch[now][1];
        }
    }
}

int build(int f,int l,int r) {
    if (l>r) return 0;
    int mid=(l+r)>>1;
    int now=++tot;
    key[now]=a[mid]; fa[now]=f; tag[now]=0;
    ch[now][0]=build(now,l,mid-1);
    ch[now][1]=build(now,mid+1,r);
    pushup(now);
    return now;
}

void turn(int l,int r) {
    l=query_kth(l); r=query_kth(r+2);
    //先让l占下根的位置,然后让r+2把他挤到左子树上去
    splay(l,0); splay(r,l);
    pushdown(rt);
    tag[ch[ch[rt][1]][0]]^=1;//根的右子树的左子树
}

void print(int now) {
    pushdown(now);
    if (ch[now][0]) print(ch[now][0]);
    if (key[now]!=-INF && key[now]!=INF) {
        if (t) printf(" ");
        printf("%d",key[now]);
        t=1;
    }
    if (ch[now][1]) print(ch[now][1]);
}

int main() {
    scanf("%d%d",&n,&m);
    for (int i=1; i<=n; i++) a[i+1]=i;
    a[1]=-INF;a[n+2]=INF;
    rt=build(0,1,n+2);
    for (int i=1; i<=m; i++) {
        int x,y;
        scanf("%d%d",&x,&y);
        turn(x,y);
    }
    print(rt);
    printf("\n");
    return 0;
}

例2:luogu4146 序列终结者

// luogu-judger-enable-o2
#include<bits/stdc++.h>
using std::max;
using std::swap;

#define gc getchar()
#define get(x) (ch[fa[x]][1]==x)
#define ls ch[x][0]
#define rs ch[x][1]

inline void read(int& t) {
    t=0; int f=1; char ch=gc;
    while (ch<'0' || ch>'9') {if (ch=='-') f=-1; ch=gc;}
    do {t=t*10+ch-'0'; ch=gc;} while (ch>='0' && ch<='9');
    t*=f;
}

const int N=100005;
int n,m,rt,tot,num[N],ch[N][2],fa[N],s[N],v[N],add[N],rev[N],mx[N];

inline void pushup(int x) {
    s[x]=s[ch[x][1]]+s[ch[x][0]]+1;
    mx[x]=max(v[x],max(mx[ch[x][1]],mx[ch[x][0]]));
}

inline void pushdown(int x) {
    if (add[x]) {
        if (ls) add[ls]+=add[x], v[ls]+=add[x], mx[ls]+=add[x];
        if (rs) add[rs]+=add[x], v[rs]+=add[x], mx[rs]+=add[x];
    }
    if (rev[x]) rev[ls]^=1, rev[rs]^=1, swap(ls,rs);
    rev[x]=add[x]=0;
} 

inline void rotate(int x) {
    int y=fa[x], z=fa[fa[x]], k=get(x);
    pushdown(y), pushdown(x);
    ch[y][k]=ch[x][k^1], fa[ch[y][k]]=y;
    ch[x][k^1]=y, fa[y]=x, fa[x]=z;
    if (z) ch[z][ch[z][1]==y]=x;
    pushup(y), pushup(x);
}

inline void splay(int x, int g) {
    for (int j; (j=fa[x])!=g; rotate(x))
        if (fa[j]!=g) rotate((get(x)==get(j))?j:x);
    if (!g) rt=x;
}

inline int build(int l,int r,int f) {
    if (l>r) return 0;
    int x=++tot, mid=(l+r)/2;
    fa[x]=f, v[x]=mx[x]=num[mid];
    ch[x][0]=build(l,mid-1,x);
    ch[x][1]=build(mid+1,r,x);
    pushup(x); return x;
}


inline int kth(int x) {
    int pos=rt;
    while (pos) {
        pushdown(pos);
        if (x<=s[ch[pos][0]]) pos=ch[pos][0];
        else {
            x-=s[ch[pos][0]]+1;
            if (!x) return pos;
            pos=ch[pos][1];
        }
    }
}

int main() {
    read(n), read(m);
    mx[0]=num[1]=num[n+2]=-2e9, rt=build(1, n+2, 0);
    while (m--) {
        int k, l, r, vv, s;
        read(k), read(l), read(r);
        int L=kth(l), R=kth(r+2);
        splay(L,0), splay(R,L);
        s=ch[ch[rt][1]][0];
        if (k==1) read(vv), add[s]+=vv, mx[s]+=vv, v[s]+=vv;
        else if (k==2) rev[s]^=1;
        else printf("%d\n",mx[s]);
    }
    return 0;
}

更多精品例题请待更新

评论

暂无评论

发表评论

可以用@mike来提到mike这个用户,mike会被高亮显示。如果你真的想打“@”这个字符,请用“@@”。