首页 技术 正文
技术 2022年11月14日
0 收藏 697 点赞 2,840 浏览 7038 个字

#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\n", rk(x));
else if (opt == 4) printf("%d\n", kth(x));
else if (opt == 5)
insert(x), printf("%d\n", t[pre()].val), del(x);
else insert(x), printf("%d\n", 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;
}
``
相关推荐
python开发_常用的python模块及安装方法
adodb:我们领导推荐的数据库连接组件bsddb3:BerkeleyDB的连接组件Cheetah-1.0:我比较喜欢这个版本的cheeta…
日期:2022-11-24 点赞:878 阅读:9,085
Educational Codeforces Round 11 C. Hard Process 二分
C. Hard Process题目连接:http://www.codeforces.com/contest/660/problem/CDes…
日期:2022-11-24 点赞:807 阅读:5,560
下载Ubuntn 17.04 内核源代码
zengkefu@server1:/usr/src$ uname -aLinux server1 4.10.0-19-generic #21…
日期:2022-11-24 点赞:569 阅读:6,409
可用Active Desktop Calendar V7.86 注册码序列号
可用Active Desktop Calendar V7.86 注册码序列号Name: www.greendown.cn Code: &nb…
日期:2022-11-24 点赞:733 阅读:6,182
Android调用系统相机、自定义相机、处理大图片
Android调用系统相机和自定义相机实例本博文主要是介绍了android上使用相机进行拍照并显示的两种方式,并且由于涉及到要把拍到的照片显…
日期:2022-11-24 点赞:512 阅读:7,819
Struts的使用
一、Struts2的获取  Struts的官方网站为:http://struts.apache.org/  下载完Struts2的jar包,…
日期:2022-11-24 点赞:671 阅读:4,902