「笔记」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 种之一:
- 插入 \(x\) 数。
- 删除 \(x\) 数(若有多个相同的数,因只删除一个)。
- 查询 \(x\) 数的排名(排名定义为比当前数小的数的个数 +\(1\))。
- 查询排名为 \(x\) 的数。
- 求 \(x\) 的前驱(前驱定义为小于 \(x\),且最大的数)。
- 求 \(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;
}
写在最后
鸣谢