[数据结构]伸展树(Splay)

#0.0 写在前面

Splay(伸展树)是较为重要的一种平衡树,理解起来也依旧很容易,但是细节是真的多QnQ,学一次忘一次,还是得用博客加深一下理解(

#1.0 Splay!

#1.1 基本构架

Splay 是如何维护树的平衡的呢?近乎不维护...但其实能使 Splay 的形态接近平衡的原因是每一次操作后都将操作的点拉到根部(splay() 操作),这似乎是基于统计学的某个结论:这次操作过的数据很有可能还是下一次的操作目标(?)不懂欸QuQ

总之,在实践中我们可以得到这样一点:Splay 能用,并且好用!这就足够了。

对于 Splay 上的每个节点,我们维护如下几个信息:

struct Tree {int son[2], f, cnt, size, val;} t[N];

此外还有一些较为常用的基本操作如下:

inline void clear(int k) { //清空 k 号节点
    t[k].cnt = t[k].f = t[k].size = t[k].son[1] = t[k].son[0] = t[k].val = 0;
}

inline void pushup(int k) { //更新 k 号点的大小
    t[k].size = t[t[k].son[0]].size + t[t[k].son[1]].size + t[k].cnt;
}

/*获取当前节点的儿子种类,0 为左儿子,1 为右儿子*/
inline int get(int k) {return k == t[t[k].f].son[1];} 

/*将 x 作为 y 的 op 儿子(0 左 1 右)连边*/
inline void connect(int x, int y, int op) {
    if (x) {t[x].f = y;} if (y) {t[y].son[op] = x;}
}

#1.2 核心操作

核心操作当然就是 splay() 操作了,它可以将一个点拉到根部。splay() 操作中将一个点上旋的操作是一次旋两个点:

  • 父亲与儿子的儿子类型相同,先旋父亲再旋儿子;
  • 父亲与儿子的儿子类型不同,先旋儿子再旋父亲;
  • 如果父亲是根,那么只旋儿子。

这里的旋转依旧是传统的左旋和右旋,但是我们不再分开讨论,将他们的共同点抽象出来,我们不难得到以下结论:

设当前要旋转的点是 (k)(f)(k) 的父亲,(ff)(f) 的父亲,(r)(k) 的儿子类型, (rf)(f) 的儿子类型,那么我们应当将 (k)(r hat{} 1) 类型的儿子作为 (f) 新的 (r) 类型的孩子,(f) 变为 (k)(r hat{} 1) 类型的儿子,(k) 作为 (ff) 的新的 (rf) 类型的孩子。

经过以上的抽象,旋转操作就变得简洁很多了。

inline void rotate(int k) {
    int f = t[k].f, ff = t[f].f, r = get(k), rf = get(f);
    connect(t[k].son[r ^ 1], f, r); connect(f, k, r ^ 1);
    connect(k, ff, rf); pushup(f); pushup(k);
}

inline void splay(int k) {
    for (int f = t[k].f; f = t[k].f, f; rotate(k))
      if (t[f].f) rotate(get(k) == get(f) ? f : k);
    rt = k;
}

#1.3 基本操作

记住一点,不论什么操作后都需要将对应的节点通过 splay() 拉至根部。

#1.3.1 插入

插入的本质就是 BST 的插入,从根节点向下,不断根据权值大小判断进入哪个子树,插入后需要将操作的节点 splay() 至根部。

inline void insert(int k) {
    if (!rt){
        rt = ++ tot, t[rt].val = k;
        t[rt].cnt ++; pushup(rt); return;
    }
    int now = rt, f = 0;
    while (true) {
        if (t[now].val == k) {
            t[now].cnt ++; pushup(now);
            pushup(f); splay(now); break;
        }
        f = now; now = t[now].son[t[now].val < k];
        if (! now) {
            t[++ tot].val = k, t[tot].f = f;
            t[tot].cnt ++; t[f].son[t[f].val < k] = tot;
            pushup(tot); pushup(f); splay(tot); break;
        }
    }
}

#1.3.2 查询排名

根据权值大小判断进入左子树还是右子树,进入右子树时需要加上当前节点的大小和左子树的大小。查询结束后进行 splay() 操作。

inline int rk(int k) {
    int res = 0, now = rt;
    while (true) {
        if (k < t[now].val) now = t[now].son[0];
        else {
            res += t[t[now].son[0]].size;
            if (k == t[now].val) splay(now), return res + 1;
            res += t[now].cnt; now = t[now].son[1];
        }
    }
}

#1.3.3 查询第 k 大

依旧是传统的通过左子树大小进行查找。

inline int kth(int k) {
    int now = rt;
    while (true) {
        if (k <= t[t[now].son[0]].size) now = t[now].son[0];
        else {
            k -= t[t[now].son[0]].size + t[now].cnt;
            if (k <= 0) splay(now), return t[now].val;
            now = t[now].son[1];
        }
    }
}

#1.3.4 前驱 & 后继

我们直接将要查前驱/后继的数插入 Splay,此时所在节点就被拉到了根部,直接找根的左/右子树的最右/左点即可,最后再删掉该点。

inline int pre() {
    int cur = t[rt].son[0];
    while (t[cur].son[1]) cur = t[cur].son[1];
    splay(cur); return cur;
}

inline int nxt() {
    int cur = t[rt].son[1];
    while (t[cur].son[0]) cur = t[cur].son[0];
    splay(cur); return cur;
}

#1.3.5 删除

主要是进行分类讨论,这一部分不多解释,详见下方代码

inline void del(int k) {
    rk(k); //调用 rk() 的主要目的是将该点拉到根部
    if (t[rt].cnt > 1) {
        t[rt].cnt --; pushup(rt); return;
    } else if (!t[rt].son[0] && !t[rt].son[1]) {
        clear(rt); rt = 0; return;
    } else if (!t[rt].son[0]) {
        int tmp = rt; rt = t[tmp].son[1];
        t[rt].f = 0; clear(tmp); return;
    } else if (!t[rt].son[1]) {
        int tmp = rt; rt = t[tmp].son[0];
        t[rt].f = 0; clear(tmp); return;
    }
    int tmp = rt, l = pre(); splay(l);
    /*分析旋转过程不难发现,前驱的右子树一定为空*/
    /*同样在最终将前驱旋转至根部时,右子树一定也为空*/
    /*所以最后要删掉点的左子树一定为空*/
    /*也可以考虑 BST 的性质,前驱和当前点之间不会再有其他点*/
    connect(t[tmp].son[1], rt, 1);
    clear(tmp); pushup(rt);
}

#2.0 经典例题

#2.1 P3369 【模板】普通平衡树

就是上面操作的结合。

const int N = 100010;
const int INF = 0x3fffffff;

struct Tree {int son[2], f, cnt, size, val;} t[N];

int rt, tot, n;

inline void clear(int k) {
    t[k].cnt = t[k].f = t[k].size = t[k].son[1] = t[k].son[0] = t[k].val = 0;
}

inline void pushup(int k) {
    t[k].size = t[t[k].son[0]].size + t[t[k].son[1]].size + t[k].cnt;
}

inline int get(int k) {return k == t[t[k].f].son[1];}

inline void connect(int x, int y, int op) {
    if (x) {t[x].f = y;} if (y) {t[y].son[op] = x;}
}

inline void rotate(int k) {
    int f = t[k].f, ff = t[f].f, r = get(k), rf = get(f);
    connect(t[k].son[r ^ 1], f, r); connect(f, k, r ^ 1);
    connect(k, ff, rf); pushup(f); pushup(k);
}

inline void splay(int k) {
    for (int f = t[k].f; f = t[k].f, f; rotate(k))
      if (t[f].f) rotate(get(k) == get(f) ? f : k);
    rt = k;
}

inline void insert(int k) {
    if (!rt){
        rt = ++ tot, t[rt].val = k;
        t[rt].cnt ++; pushup(rt); return;
    }
    int now = rt, f = 0;
    while (true) {
        if (t[now].val == k) {
            t[now].cnt ++; pushup(now);
            pushup(f); splay(now); break;
        }
        f = now; now = t[now].son[t[now].val < k];
        if (! now) {
            t[++ tot].val = k, t[tot].f = f;
            t[tot].cnt ++; t[f].son[t[f].val < k] = tot;
            pushup(tot); pushup(f); splay(tot); break;
        }
    }
}

inline int rk(int k) {
    int res = 0,now = rt;
    while (true) {
        if (k < t[now].val) now = t[now].son[0];
        else {
            res += t[t[now].son[0]].size;
            if (k == t[now].val) splay(now), return res + 1;
            res += t[now].cnt; now = t[now].son[1];
        }
    }
}

inline int kth(int k) {
    int now = rt;
    while (true) {
        if (k <= t[t[now].son[0]].size) now = t[now].son[0];
        else {
            k -= t[t[now].son[0]].size + t[now].cnt;
            if (k <= 0) splay(now), return t[now].val;
            now = t[now].son[1];
        }
    }
}

inline int pre() {
    int cur = t[rt].son[0];
    while (t[cur].son[1]) cur = t[cur].son[1];
    splay(cur); return cur;
}

inline int nxt() {
    int cur = t[rt].son[1];
    while (t[cur].son[0]) cur = t[cur].son[0];
    splay(cur); return cur;
}

inline void del(int k) {
    rk(k);
    if (t[rt].cnt > 1) {
        t[rt].cnt --; pushup(rt); return;
    } else if (!t[rt].son[0] && !t[rt].son[1]) {
        clear(rt); rt = 0; return;
    } else if (!t[rt].son[0]) {
        int tmp = rt; rt = t[tmp].son[1];
        t[rt].f = 0; clear(tmp); return;
    } else if (!t[rt].son[1]) {
        int tmp = rt; rt = t[tmp].son[0];
        t[rt].f = 0; clear(tmp); return;
    }
    int tmp = rt, l = pre(); splay(l);
    connect(t[tmp].son[1], rt, 1);
    clear(tmp); pushup(rt);
}

int main() {
    scanf("%d", &n); int opt,x;
    while (n --){
        scanf("%d%d", &opt, &x);
        if (opt == 1) insert(x);
        else if (opt == 2) del(x);
        else if (opt == 3) printf("%d
", rk(x));
        else if (opt == 4) printf("%d
", kth(x));
        else if (opt == 5)
          insert(x), printf("%d
", t[pre()].val), del(x);
        else insert(x), printf("%d
", t[nxt()].val), del(x);
    }
    return 0;
}

#2.2 P3391 【模板】文艺平衡树

这题用到了 Splay 的另一个应用:处理序列。

对于每个节点,我们再多维护一个 tag,表示当前节点的子树的所有节点的左右儿子是否交换,通过两次 splay() 操作可以将操作区间提取出来。注意在旋转之前需要将当前节点和父亲节点的标记下传。

const int N = 400010;
const int INF = 0x3fffffff;

struct Tree {int son[2], f, cnt, size, val, tg;} t[N];

int rt,tot, n,m;

inline void clear(int k) {
    t[k].cnt = t[k].f = t[k].size = t[k].son[1] = t[k].son[0] = t[k].val = 0;
}

inline void pushup(int k) {
    t[k].size = t[t[k].son[0]].size + t[t[k].son[1]].size + t[k].cnt;
}

inline void pushdown(int k) {
    if (t[k].tg){
        t[t[k].son[0]].tg ^= 1; t[t[k].son[1]].tg ^= 1;
        swap(t[k].son[0],t[k].son[1]); t[k].tg = 0;
    }
}

inline int get(int k) {return k == t[t[k].f].son[1];}

inline void connect (int x, int y, int op){
    if (x) {t[x].f = y;} if (y) {t[y].son[op] = x;}
}

inline void rotate(int k) {
    pushdown(t[k].f), pushdown(k),;
    int f = t[k].f, ff = t[f].f, r = get(k), rf = get(f);
    connect(t[k].son[r ^ 1], f, r); connect(f, k, r ^ 1);
    connect(k, ff, rf); pushup(f); pushup(k);
}

inline void splay(int k, int g) {
    for (int f = t[k].f; f = t[k].f, f != g; rotate(k))
      if (t[f].f != g) rotate(get(k) == get(f) ? f : k);
    if (!g) rt = k;
}

inline int kth(int k) {
    int now = rt;
    while (true){
        pushdown(now);
        if (t[t[now].son[0]].size >= k)
          now = t[now].son[0];
        else{
            k -= t[t[now].son[0]].size + t[now].cnt;
            if (k <= 0) return now;
            now = t[now].son[1];
        }
    }
}

inline void reverse(int x, int y) {
    int l = x - 1, r = y + 1;
    l = kth(l + 1); r = kth(r + 1);
    splay(l, 0); splay(r, l);
    int cur = t[rt].son[1];
    cur = t[cur].son[0];
    t[cur].tg ^= 1;
}

inline void print(int k) {
    pushdown(k);
    if (t[k].son[0]) print(t[k].son[0]);
    if (t[k].val != 0 && t[k].val != n + 1)
      printf("%d ",t[k].val);
    if (t[k].son[1]) print(t[k].son[1]);
}

inline void build(int l, int r, int k, int f) {
    if (l > r) return;
    if (l == r){
        t[k].f = f; t[k].val = l;
        t[k].cnt = t[k].size = 1;
        return;
    }
    int mid = (l + r) >> 1;
    t[k].f = f; t[k].val = mid; t[k].cnt = 1;
    t[k].son[0] = ++ tot; build(l, mid - 1, tot, k);
    t[k].son[1] = ++ tot; build(mid + 1, r, tot, k);
    pushup(k);
}

int main(){
    scanf("%d%d", &n, &m);
    build(0, n + 1, ++ tot, 0);
    rt = 1;
    while (m --){
        int x, y; scanf("%d%d", &x, &y); reverse(x, y);
    }
    print(rt);
    return 0;
}
``
原文地址:https://www.cnblogs.com/Dfkuaid-210/p/splay.html