「笔记」Splay

写在前面

2020.5.17 时,Luckyblock 发表了一篇名为《平衡树从入门到黑题》的博文,它被曾经的 Luckyblock 视为「最高之作」,甚至起了个这么个无比中二的名字。
如今回首过去,看着曾经写下的青涩的文字,回想起走过的快两年的欢欣与痛苦并存的路……

此文用于自我复习,Splay 初学者请移步其它博客。

简介

前置知识:二叉搜索树

Splay 是一种二叉搜索树的维护方式。它通过不断将某节点旋转到根节点,使得整棵树仍满足二叉搜索树的性质,且保持平衡不至于退化为链,以保证复杂度。

旋转

用于将指定节点上移一个位置。
分为左旋和右旋两种,右旋在指定节点为左儿子时使用,左旋反之。二者的目的相同,并无本质区别,如下图所示:

大鸟转转转

旋转操作有如下要求:

  • 整棵树的中序遍历不变(不破坏二叉搜索树的性质)。
  • 节点维护的信息依然正确有效。
  • root 必须指向旋转后的根节点。

代码如下:

void Rotate(int now_) {
  int fa_ = f, whichson = WhichSon(now_);
  if (fa[f]) son[fa[f]][WhichSon(f)] = now_; //更新祖父
  f = fa[f];

  son[fa_][whichson] = son[now_][whichson ^ 1]; //更新 fa 的儿子
  fa[son[fa_][whichson]] = fa_;

  son[now_][whichson ^ 1] = fa_; //上提 now_
  fa[fa_] = now_;
  Pushup(fa_), Pushup(now_);
}

旋转到根节点

Splay 规定,插入/查询一个节点后,要将其旋转到根节点,以保持平衡结构。
一种直观的想法是不断上提指定节点,使其到达根节点。这种方法叫做单旋。
对应的就有双旋,每次对上提的节点和其父亲分六种情况讨论,若父亲为根直接上旋(1,2)。否则考察父亲的儿子类型与指定节点的儿子类型是否相同,若相同则先转父亲再转儿子(3,4),否则转两次儿子(5,6)。

大鸟转转转

画个图之后很容易发现两种方法的不同:

可以发现,在链状数据上,每轮双旋都会产生一个分叉点,最后会使得树的高度减少一半,从而使二叉搜索树近于平衡。而单旋后树高不变,二叉搜索树仍是不平衡的,会被恶意构造数据的凉心出题人卡掉。

void Splay(int now_) {
  for (; f != 0; Rotate(now_)) {
    if (fa[f]) Rotate(WhichSon(now_) == WhichSon(f) ? f : now_);
  }
  root = now_;
}

封装模板

以上两个操作使得 Splay 可以维护平衡性。之后就可以将 Splay 简单地当作一棵二叉搜索树使用了。
以下给出简单易懂的带有注释的封装后的模板。

namespace Splay {
  #define f fa[now_]
  #define ls son[now_][0]
  #define rs son[now_][1]
  const int kMaxNode = 1e6 + 10;
  int root, node_num, fa[kMaxNode], son[kMaxNode][2];
  int val[kMaxNode], cnt[kMaxNode], siz[kMaxNode];
  int top, bin[kMaxNode];
  void Pushup(int now_) { //更新节点大小信息
    if (!now_) return ;
    siz[now_] = cnt[now_];
    if (ls) siz[now_] += siz[ls];
    if (rs) siz[now_] += siz[rs];
  }
  int WhichSon(int now_) { //获得儿子类型
    return now_ == son[f][1];
  }
  void Clear(int now_) { //清空节点,同时进行垃圾回收
    f = ls = rs = val[now_] = cnt[now_] = siz[now_] = 0;
    bin[++ top] = now_;
  }
  int NewNode(int fa_, int val_) { //建立新节点
    int now_ = top ? bin[top --] : ++ node_num; //优先调用垃圾堆
    f = fa_, val[now_] = val_, siz[now_] = cnt[now_] = 1;
    return now_;
  }
  void Rotate(int now_) { //旋转操作
    int fa_ = f, whichson = WhichSon(now_);
    if (fa[f]) son[fa[f]][WhichSon(f)] = now_;
    f = fa[f];

    son[fa_][whichson] = son[now_][whichson ^ 1];
    fa[son[fa_][whichson]] = fa_;

    son[now_][whichson ^ 1] = fa_;
    fa[fa_] = now_;
    Pushup(fa_), Pushup(now_);
  }
  void Splay(int now_) { //旋转到根操作
    for (; f != 0; Rotate(now_)) {
      if (fa[f]) Rotate(WhichSon(now_) == WhichSon(f) ? f : now_);
    }
    root = now_;
  }
  void Insert(int now_, int fa_, int val_) { //插入操作
    if (now_ && val[now_] != val_) {
      Insert(son[now_][val[now_] < val_], now_, val_);
      return ;
    }
    if (val[now_] == val_) ++ cnt[now_]; //已存在
    if (!now_) { //不存在
      now_ = NewNode(fa_, val_);
      if (f) son[f][val[f] < val_] = now_;
    }
    Pushup(now_), Pushup(f), Splay(now_); //注意旋转到根
  }
  int Find(int now_, int val_) { //将权值 val 对应节点旋转至根。在平衡树上二分。
    if (!now_) return false; //不存在
    if (val_ < val[now_]) return Find(ls, val_);
    if (val_ == val[now_]) {
      Splay(now_);
      return true;
    }
    return Find(rs, val_);
  }
  void Delete(int val_) { //删除一个权值 val 
    if (!Find(root, val_)) return ; //将 val 转到根
    if (cnt[root] > 1) {
      -- cnt[root];
      Pushup(root);
      return ;
    }
    int oldroot = root;
    if (!son[root][0] && !son[root][1]) {
      root = 0;
    } else if (!son[root][0]) { 
      root = son[root][1], fa[root] = 0;
    } else if (!son[root][1]) {
      root = son[root][0], fa[root] = 0;
    } else if (son[root][0] && son[root][1]) {
      //将中序遍历中 root 前的一个元素作为新的 root。该元素即为 root 左子树中最大的元素。
      int leftmax = son[root][0];
      while (son[leftmax][1]) leftmax = son[leftmax][1];
      Splay(leftmax); //转到根
      son[root][1] = son[oldroot][1], fa[son[root][1]] = root; //继承信息
    }
    Clear(oldroot), Pushup(root);
  }
  int QueryRank(int val_) { //查询 val_ 的排名
    Insert(root, 0, val_); //先插入,将其转到根,查询左子树大小
    int ret = siz[son[root][0]] + 1;
    Delete(val_);
    return ret;
  }
  int QueryVal(int rk_) { //查询排名为 rk 的权值。在平衡树上二分。
    int now_ = root;
    while (true) {
      if (!now_) return -1;
      if (ls && siz[ls] >= rk_) { //注意 =
        now_ = ls;
      }else {
        rk_ -= ls ? siz[ls] : 0;
        if (rk_ <= cnt[now_]) { //该权值即为 val[now_]
          Splay(now_);
          return val[now_];
        }
        rk_ -= cnt[now_];
        now_ = rs;
      }
    }
  }
  int QueryPre(int val_) { //查询前驱
    Insert(root, 0, val_); //插入 val_,将其旋转到根,前驱(中序遍历中前一个元素)即为左子树中最大的元素。
    int now_ = son[root][0];
    while (rs) now_ = rs;
    Delete(val_);
    return val[now_];
  }
  int QueryNext(int val_) { //查询后继
    Insert(root, 0, val_);
    int now_ = son[root][1];
    while (ls) now_ = ls;
    Delete(val_);
    return val[now_];
  }
}

维护序列

构建 Splay

考虑以在数列的元素的先后顺序作为 Splay 构造时的参数,使得 Splay 的中序遍历为原数列。这样原序列的一个子串就能和 Splay 上的一个连通子树相对应。
构建类似线段树,递归调用 Build 函数即可,如下所示:

void Build(int &now_, int fa_, int L_, int R_) {
  if (L_ > R_) return;
  int mid = (L_ + R_) >> 1; 
  now_ = NewNode(fa_, mid);
  Build(ls, now_, L_, mid - 1);
  Build(rs, now_, mid + 1, R_);
  Pushup(now_);
}

单点插入

将元素 \(x\) 插入到元素 \(y\) 的前面,即使得 \(x\) 成为 \(y\) 的前驱。在平衡树中找到 \(y\) 所在子树,使 \(x\) 成为其左子树中最右侧节点即可。

区间插入

与单点插入同理,使得插入的区间构造出的 Splay 下挂到其后继节点左子树中最右侧节点即可。

区间修改

设修改区间为 \([l,r]\),考虑将被操作区间放置到一棵单独的子树内,对整棵子树打标记实现区间修改。

考虑\(l-1\) 对应节点转到根节点,此时根左子树代表储存序列 \([1,l-2]\),右子树代表序列 \([l,n]\)\(r\) 对应节点在右子树中。
发现 Splay 操作不一定要转到根节点,是可以钦定旋转的终点的。考虑将 \(r+1\) 转到 \(l-1\) 的右儿子位置,此时 \(r +1\) 的左儿子即对应区间 \([l,r]\)。对整棵子树打标记即可。

实现是一般添加两个虚拟节点 \(0\)\(n+1\),此时在中序遍历中原数列各元素的排名都增加了 1,需要添加偏移量。

void Modify(int L_, int R_) {
  int x = Kth(root, L_ - 1 + 1), y = Kth(root, R_ + 1 + 1); //找到被修改的节点,注意添加偏移量
  Splay(x, 0), Splay(y, x);
  tag[son[son[root][1]][0]] = someting; //打标记
}

区间查询

按照上述方法将 \([l,r]\) 放置到一棵单独的子树内,查询整棵子树的信息即可。

复杂度

单次操作都是 \(O(\log n)\) 的。

不 会 势 能 分 析 证 明
建议百度。

例题

P3369 【模板】普通平衡树

您需要写一种数据结构来维护一些数。
\(n\) 次操作,每种操作是下列 6 种之一:

  1. 插入 \(x\) 数。
  2. 删除 \(x\) 数(若有多个相同的数,因只删除一个)。
  3. 查询 \(x\) 数的排名(排名定义为比当前数小的数的个数 +\(1\))。
  4. 查询排名为 \(x\) 的数。
  5. \(x\) 的前驱(前驱定义为小于 \(x\),且最大的数)。
  6. \(x\) 的后继(后继定义为大于 \(x\),且最小的数)。

\(1\le n\le 10^5\)\(|x|\le 10^7\)
1S,128MB。

把上面封装的模板拿来用就行了。

P3391 【模板】文艺平衡树

给定一长度为 \(n\) 的有序序列 \(a\),初始时有 \(\forall 1\le i\le n,\, a_i = i\)。给定 \(m\) 次操作。
每次操作给定区间 \([l,r]\),表示将区间 \([l,r]\) 反转。
输出 \(k\) 次操作后的数列。
\(1\le n,m\le 10^5\)
1S,128MB。

使用上面的套路提取区间,进行区间修改即可。注意标记下放的写法,反转标记取反并交换左右儿子。
复杂度 \(O(m\log n)\) 级别。

//知识点:Splay
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
int n, m;
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
namespace Splay {
  #define f fa[now_]
  #define ls son[now_][0]
  #define rs son[now_][1]
  const int kMaxNode = kN;
  int root, node_num, fa[kMaxNode], son[kMaxNode][2];
  int val[kMaxNode], siz[kMaxNode], cnt[kMaxNode];
  bool tag[kN];
  int top, bin[kMaxNode];
  void Pushup(int now_) {
    if (!now_) return ;
    siz[now_] = cnt[now_];
    if (ls) siz[now_] += siz[ls];
    if (rs) siz[now_] += siz[rs];
  }
  void Pushdown(int now_) {
    if (!now_ || !tag[now_]) return ;
    tag[ls] ^= 1, tag[rs] ^= 1;
    std::swap(ls, rs);
    tag[now_] = false;
  }
  void Clear(int now_) {
    f = ls = rs = val[now_] = cnt[now_] = siz[now_] = 0;
    bin[++ top] = now_;
  }
  int NewNode(int fa_, int val_) {
    int now_ = top ? bin[top --] : ++ node_num;
    f = fa_, val[now_] = val_, cnt[now_] = siz[now_] = 1;
    return now_;
  }
  bool WhichSon(int now_) {
    return now_ == son[f][1];
  }
  void Rotate(int now_) {
    int fa_ = f, whichson = WhichSon(now_);
    if (fa[f]) son[fa[f]][WhichSon(f)] = now_;
    f = fa[f];

    son[fa_][whichson] = son[now_][whichson ^ 1];
    fa[son[fa_][whichson]] = fa_;

    son[now_][whichson ^ 1] = fa_;
    fa[fa_] = now_;
    Pushup(fa_), Pushup(now_);
  }
  void Splay(int now_, int fa_) {
    for (; f != fa_; Rotate(now_)) {
      if (fa[f] != fa_) Rotate(WhichSon(f) == WhichSon(now_) ? f : now_); 
    }
    if (!fa_) root = now_;
  }
  void Build(int &now_, int fa_, int L_, int R_) {
    if (L_ > R_) return;
    int mid = (L_ + R_) >> 1; 
    now_ = NewNode(fa_, mid);
    Build(ls, now_, L_, mid - 1);
    Build(rs, now_, mid + 1, R_);
    Pushup(now_);
  }
  int Kth(int now_, int rk_) {
    Pushdown(now_);
    if (ls && rk_ <= siz[ls]) return Kth(ls, rk_);
    if (rk_ <= siz[ls] + cnt[now_]) return now_;
    return Kth(rs, rk_ - siz[ls] - cnt[now_]);
  }
  void Modify(int L_, int R_) {
    int x = Kth(root, L_ - 1 + 1), y = Kth(root, R_ + 1 + 1); //偏移量
    Splay(x, 0), Splay(y, x);
    tag[son[son[root][1]][0]] ^= 1;
  }
  void Print(int now_) {
    Pushdown(now_);
    if (ls) Print(ls);
    if (val[now_] && val[now_] <= n) printf("%d ", val[now_]);
    if (rs) Print(rs);
  }
}
//=============================================================
int main() { 
  n = read(), m = read();
  Splay::Build(Splay::root, 0, 0, n + 1);
  while (m --) {
    int l = read(), r = read();
    Splay::Modify(l ,r);
  }
  Splay::Print(Splay::root);
  return 0; 
}

写在最后

鸣谢

Splay - OI Wiki

posted @ 2021-01-22 14:51  Luckyblock  阅读(221)  评论(2编辑  收藏  举报