• [数据结构]伸展树(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;
    }
    ``
  • 相关阅读:
    1094. Car Pooling
    121. Best Time to Buy and Sell Stock
    58. Length of Last Word
    510. Inorder Successor in BST II
    198. House Robber
    57. Insert Interval
    15. 3Sum java solutions
    79. Word Search java solutions
    80. Remove Duplicates from Sorted Array II java solutions
    34. Search for a Range java solutions
  • 原文地址:https://www.cnblogs.com/Dfkuaid-210/p/splay.html
Copyright © 2020-2023  润新知