P3391 【模板】文艺平衡树 · Splay

题解

离上一次写Splay已经过去很久了,所以我很理所当然地忘了orz…


先讲一下做这道题的前置技能:
主要是给上一次没有好好学的自己和没过多久肯定会忘的自己看的

百度百科 - 伸展树
Splay伸展树,也叫分裂树,是一种二叉排序树,它能在O(log n)内完成插入、查找和删除操作
在伸展树上的一般操作都基于伸展操作:假设想要对一个二叉查找树执行一系列的查找操作,为了使整个查找时间更小,被查频率高的那些条目就应当经常处于靠近树根的位置。于是想到设计一个简单方法, 在每次查找之后对树进行重构,把被查找的条目搬移到离树根近一些的地方。伸展树应运而生。伸展树是一种自调整形式的二叉查找树,它会沿着从某个节点到树根之间的路径,通过一系列的旋转把这个节点搬移到树根去。
它的优势在于不需要记录用于平衡树的冗余信息。

Splay最大的特点就是旋转,每次操作都要旋转,

Splay树的结构不是平衡的,只是通过双转rotate构造随机的树结构来保持均摊复杂度是 O ( log ⁡ n ) O(\log n) O(logn)的.

但不管怎么旋转其节点的值永远满足二叉搜索树的性质:
当前节点的左儿子的值比该节点的值小,当前节点右儿子的值比该节点的值大,不存在相等的情况

Splay的具体操作这里就不重复了,如果不会可以参考下面的博客
bzoj3224: Tyvj 1728 普通平衡树(平衡树)

↑与这道题有关的几个函数:
inline bool get(int x); //用于求出当前节点是其父节点的哪个儿子
inline void update(int x); //在每次旋转后更新子树大小
inline void rotate(int x); //节点x与其父节点与父节点的父节点之间的旋转
inline void splay(int x); //旋转x到根节点
inline void insert(int x); //插入节点x
inline int findx(int x); //查询排名为x的节点


参考洛谷前三篇题解↓
洛谷入口 : P3391 【模板】文艺平衡树 题解

记几个要点:

Splay可以用来维护序列,这是把Splay当作一棵区间树,

看到区间树首先想到的就是线段树,对吧?

看到洛谷里有大佬用线段树做的,不过因为题目给出的数组是1~n顺序存储的,所以这里直接用放Splay树里了

前面说到Splay满足二叉搜索树的性质,所以每个节点存的就不可能是题目给出的值,

再根据大佬给出的提示:如果一个点在序列中的位置为第K个,那么,他就是二叉搜索树的第K大,

所以Splay树里其实放的是数组的下标(只不过在这道题里面下标和值刚好相等),比如说这样:
在这里插入图片描述

再再根据大佬给出的提示:可以利用splay的性质,翻转区间 [ l , r ] [l,r] [l,r]时,先将第 l − 1 l-1 l1 大的翻到根节点,再把第 r + 1 r+1 r+1 大的节点翻到根节点的右儿子的位置,将 l 、 r l、r lr 所在的子树放到同一个父节点下,最后交换这两棵子树,再中序遍历输出(左中右),就是我们翻转一次区间要的答案了

由于每次翻转需要第 l − 1 、 r + 1 l-1、r+1 l1r+1大的数,为了应付类似翻转区间 [ 1 , n ] [1,n] [1,n]这样的情况,在额外加入两个节点,最后输出时减1就行了,

不过交换子树后会破坏二叉搜索树的性质,并且区间操作次数太多了高达1e5,所以开个lazy[]打个标记,lazy[u]表示节点u的两个子节点是否需要交换,0不需要,1需要,

Splay能维护序列反转也是它作为LCT的辅助树的条件之一


在这里插入图片描述


代码

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n, m;
#define between(x, a, b) (a<=x && x<=b)

namespace Splay {

    int f[N];//父节点
    int ch[N][2];//俩儿子
    int key[N];//val 关键字 根绝key[]判断该节点在树里的位置
    int size[N];//包括i的子树的大小
    int sz;//整棵树的大小 同时也是编号
    int root = 0;

    int lazy[N];//做个懒标记

    inline int rank_value(int x);
    void pushdown(int u);

    inline bool get(int x) {//判断当前节点是它父节点的左儿子还是右儿子
        return ch[f[x]][1] == x;
    }

    inline void update(int x) {//更新size 用于发生修改之后
        if (x) {
            size[x] = 1;
            if (ch[x][0]) size[x] += size[ch[x][0]];
            if (ch[x][1]) size[x] += size[ch[x][1]];
        }
    }

    inline void rotate(int x) {
        int old = f[x];//x的父节点
        int oldf = f[old];//父节点的父节点
        int whichx = get(x);

        //假设x是old左儿子 现在要使旋转后 x是old父亲
        //旋转的时候 把x的右儿子给old作为左儿子
        ch[old][whichx] = ch[x][whichx ^ 1];
        f[ch[old][whichx]] = old;
        //这个时候x的新的右儿子就是old 其父亲就是原来父亲的父亲
        ch[x][whichx ^ 1] = old;//old成为x的儿子
        f[old] = x;
        f[x] = oldf;

        //然后修改oldf的儿子信息 本来是old的地方现在是x了
        if (oldf)
            ch[oldf][ch[oldf][1] == old] = x;

        update(old);
        update(x);
    }

    inline void splay(int x) { //把x旋转到根节点 //这个函数其实等同于后面的Splay(x,goal) 直接看那个吧
        for (int fa; (fa = f[x]); rotate(x)) {
            //fa先取f[x] 如果f[fa]存在的话 也就是三代关系 需要先旋转f[x]
            //如果不存在 只有两代 就先旋转x本身
            if (f[fa])
                rotate((get(x) == get(fa)) ? fa : x);
        }
        root = x;
    }

    inline void insert(int x) {//插入
        if (root == 0) {//空树
            sz++;
            ch[sz][0] = ch[sz][1] = f[sz] = 0;
            root = sz;
            size[sz] = 1;
            key[sz] = x;
            return;
        }

        int now = root;
        int fa = 0;
        while (1) {
            fa = now;
            now = ch[now][key[now] < x];
            if (now == 0) {//创建新的节点
                sz++;
                ch[sz][0] = ch[sz][1] = 0;
                f[sz] = fa;
                //size[sz] = cnt[sz] = 1;
                size[sz]=1;
                ch[fa][key[fa] < x] = sz;
                key[sz] = x;
                update(fa);
                splay(sz);
                break;
            }
        }
    }

    inline int rank_value(int x) { //找到排名为x的节点
        int now = root;
        while (1) {
            //如果有修改 先修改 类似线段树的update操作 对区间操作之前先看看区间需不需要更新
            pushdown(now);

            if (ch[now][0] && x <= size[ch[now][0]])
                now = ch[now][0];
            else {
                int tmp = (ch[now][0] ? size[ch[now][0]] : 0) + 1;
                if (x <= tmp) return key[now];
                x -= tmp;
                now = ch[now][1];
            }
        }
    }

    void Splay(int x, int goal) { //将节点x旋转到节点goal的右儿子的位置 是右儿子!不是goal本身!
        for (int fa; (fa = f[x]) != goal; rotate(x)) {
            if (f[fa] != goal)
                rotate((get(x) == get(fa)) ? fa : x); 
                // 对于x旋转到f[fa]有两种状态
                // 第一种: X和fa分别是fa和f[fa]的同一个儿子 - 先旋转fa再旋转X
				// 第二种: X和fa分别是fa和f[fa]不同的儿子 - 旋转两次X
        }
        if (goal == 0)
            root = x;
    }

    void reverse(int l, int r) {//旋转区间
        l = rank_value(l); //其实就是找到第l-1大的节点
        r = rank_value(r + 2); //找到第r+1大的节点
        Splay(l, 0); //将l-1翻到根节点位置 注意了 根节点的位置是 ch[0][1] 不是0啊
        Splay(r, l); //将r+1翻到l-1的右儿子的位置
        lazy[ch[ch[root][1]][0]] ^= 1;
        //给l-1的右儿子的左儿子 打上标记  等同于 lazy[ch[r][0]]^=1;
    }

    void pushdown(int u) {
        if (lazy[u]) {
            lazy[ch[u][0]] ^= 1;
            lazy[ch[u][1]] ^= 1;
            lazy[u] = 0;
            swap(ch[u][0], ch[u][1]);
        }
    }

    void ord_print(int u) {//中序遍历 左中右
        pushdown(u);
        if (ch[u][0])
            ord_print(ch[u][0]);

        if (between(key[u], 2, n + 1)) //1~n
            cout << key[u] - 1 << ' ';

        if (ch[u][1])
            ord_print(ch[u][1]);
    }
}
using namespace Splay;


int main() {
    ios::sync_with_stdio(0);

    cin >> n >> m;
    for (int i = 1; i <= n + 2; ++i) {//多加入两个标兵点 0 n+1
        insert(i); //节点i对应的值其实是i-1
    }

    int l, r;
    while (m--) {
        cin >> l >> r;
        reverse(l, r);
    }

    ord_print(root);//中序遍历
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当然可以,以下是一个简单的 Splay 平衡树实现代码(C++): ```c++ #include <iostream> using namespace std; struct Node { int val; Node *left, *right, *p; Node(int v) : val(v), left(nullptr), right(nullptr), p(nullptr) {} }; class SplayTree { private: Node *root; void left_rotate(Node *x) { Node *y = x->right; x->right = y->left; if (y->left != nullptr) { y->left->p = x; } y->p = x->p; if (x->p == nullptr) { root = y; } else if (x == x->p->left) { x->p->left = y; } else { x->p->right = y; } y->left = x; x->p = y; } void right_rotate(Node *x) { Node *y = x->left; x->left = y->right; if (y->right != nullptr) { y->right->p = x; } y->p = x->p; if (x->p == nullptr) { root = y; } else if (x == x->p->left) { x->p->left = y; } else { x->p->right = y; } y->right = x; x->p = y; } void splay(Node *x) { while (x->p != nullptr) { if (x->p->p == nullptr) { if (x == x->p->left) { right_rotate(x->p); } else { left_rotate(x->p); } } else if (x == x->p->left && x->p == x->p->p->left) { right_rotate(x->p->p); right_rotate(x->p); } else if (x == x->p->right && x->p == x->p->p->right) { left_rotate(x->p->p); left_rotate(x->p); } else if (x == x->p->right && x->p == x->p->p->left) { left_rotate(x->p); right_rotate(x->p); } else { right_rotate(x->p); left_rotate(x->p); } } } public: SplayTree() : root(nullptr) {} Node* search(int v) { Node *x = root; while (x != nullptr && x->val != v) { if (v < x->val) { x = x->left; } else { x = x->right; } } if (x != nullptr) { splay(x); } return x; } void insert(int v) { Node *z = new Node(v); Node *y = nullptr; Node *x = root; while (x != nullptr) { y = x; if (z->val < x->val) { x = x->left; } else { x = x->right; } } z->p = y; if (y == nullptr) { root = z; } else if (z->val < y->val) { y->left = z; } else { y->right = z; } splay(z); } void remove(int v) { Node *z = search(v); if (z == nullptr) { return; } if (z->left == nullptr) { transplant(z, z->right); } else if (z->right == nullptr) { transplant(z, z->left); } else { Node *y = minimum(z->right); if (y->p != z) { transplant(y, y->right); y->right = z->right; y->right->p = y; } transplant(z, y); y->left = z->left; y->left->p = y; } delete z; } Node* minimum(Node *x) { while (x->left != nullptr) { x = x->left; } return x; } Node* maximum(Node *x) { while (x->right != nullptr) { x = x->right; } return x; } void inorder_walk() { inorder_walk(root); cout << endl; } private: void transplant(Node *u, Node *v) { if (u->p == nullptr) { root = v; } else if (u == u->p->left) { u->p->left = v; } else { u->p->right = v; } if (v != nullptr) { v->p = u->p; } } void inorder_walk(Node *x) { if (x != nullptr) { inorder_walk(x->left); cout << x->val << ' '; inorder_walk(x->right); } } }; int main() { SplayTree tree; tree.insert(5); tree.insert(2); tree.insert(8); tree.insert(1); tree.insert(3); tree.insert(7); tree.insert(9); tree.inorder_walk(); // output: 1 2 3 5 7 8 9 tree.remove(5); tree.inorder_walk(); // output: 1 2 3 7 8 9 tree.search(7); tree.inorder_walk(); // output: 1 2 3 7 8 9 return 0; } ``` 以上代码实现了 Splay 平衡树的基本操作,包括插入、删除、查找、中序遍历等。如果你需要进一步了解 Splay 平衡树,可以参考相关资料。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值