[数据结构]伸展树(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\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;
}
``
[数据结构]伸展树(Splay)的更多相关文章
- 纸上谈兵: 伸展树 (splay tree)[转]
作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 我们讨论过,树的搜索效率与树的深度有关.二叉搜索树的深度可能为n,这种情况下,每 ...
- K:伸展树(splay tree)
伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,它能在O(lgN)内完成插入.查找和删除操作.在伸展树上的一般操作都基于伸展操作:假设想要对一个二叉查找树执行一系列的查找操作,为了使 ...
- 高级搜索树-伸展树(Splay Tree)
目录 局部性 双层伸展 查找操作 插入操作 删除操作 性能分析 完整源码 与AVL树一样,伸展树(Splay Tree)也是平衡二叉搜索树的一致,伸展树无需时刻都严格保持整棵树的平衡,也不需要对基本的 ...
- 树-伸展树(Splay Tree)
伸展树概念 伸展树(Splay Tree)是一种二叉排序树,它能在O(log n)内完成插入.查找和删除操作.它由Daniel Sleator和Robert Tarjan创造. (01) 伸展树属于二 ...
- 【BBST 之伸展树 (Splay Tree)】
最近“hiho一下”出了平衡树专题,这周的Splay一直出现RE,应该删除操作指针没处理好,还没找出原因. 不过其他操作运行正常,尝试用它写了一道之前用set做的平衡树的题http://codefor ...
- 伸展树(Splay tree)的基本操作与应用
伸展树的基本操作与应用 [伸展树的基本操作] 伸展树是二叉查找树的一种改进,与二叉查找树一样,伸展树也具有有序性.即伸展树中的每一个节点 x 都满足:该节点左子树中的每一个元素都小于 x,而其右子树中 ...
- [Splay伸展树]splay树入门级教程
首先声明,本教程的对象是完全没有接触过splay的OIer,大牛请右上角.. 首先引入一下splay的概念,他的中文名是伸展树,意思差不多就是可以随意翻转的二叉树 PS:百度百科中伸展树读作:BoGa ...
- 伸展树Splay【非指针版】
·伸展树有以下基本操作(基于一道强大模板题:codevs维护队列): a[]读入的数组;id[]表示当前数组中的元素在树中节点的临时标号;fa[]当前节点的父节点的编号;c[][]类似于Trie,就是 ...
- ZOJ 3765 Lights (zju March I)伸展树Splay
ZJU 三月月赛题,当时见这个题目没辙,没学过splay,敲了个链表TLE了,所以回来好好学了下Splay,这道题目是伸展树的第二题,对于伸展树的各项操作有了更多的理解,这题不同于上一题的用指针表示整 ...
随机推荐
- hdu-1299 Diophantus of Alexandria(分解素因子)
思路: 因为x,y必须要大与n,那么将y设为(n+k);那么根据等式可求的x=(n2)/k+n;因为y为整数所以k要整除n*n; 那么符合上面等式的x,y的个数就变为求能被n*n整除的数k的个数,且k ...
- uniapp中拿到base64转blob对象,或base64转bytes字节数组,io操作写入字节流文件bytes
1. uniAPP中拿到附件的base64如何操作,如word文件 /*** 实现思路:* 通过native.js的io操作创建文件,拿到平台绝对路径* 再通过原生类进行base64解码,拿到字节流b ...
- Linux进程管理之基本指令
目录 基本介绍 显示系统执行的进程 指令 ps - aux 常用选项 每行栏目的含义 查看父进程 终止进程 相关指令 实用案例 踢掉某个非法登录用户 终止远程登录服务sshd,在适当的时候再次重启ss ...
- 什么是NaN?它的类型是什么?如何可靠的测试一个值是否等于NaN?
NaN属性表示"不是数字"的值.这个特殊值是由于一个操作数是非数字的(例如"abc"/4)或者因为操作的结果是非数字而无法执行的. 虽然看起来很简单,但是NaN ...
- .net core的Swagger接口文档使用教程(二):NSwag
上一篇介绍了Swashbuckle ,地址:.net core的Swagger接口文档使用教程(一):Swashbuckle 讲的东西还挺多,怎奈微软还推荐了一个NSwag,那就继续写吧! 但是和Sw ...
- JdbcTemplate 基本使用
简介 JdbcTemplate 是 Spring 对 JDBC 的封装,目的是使 JDBC 更加易于使用.JdbcTemplate 是 Spring 的一部分.JdbcTemplate 处理了资源的建 ...
- html基础 表格的相关属性使用
1.1表格的基本标签 语法结构:<table> /*整体包裹部分,包裹多个tr */ <tr> /* 表格的每一个行,包裹td */ <td></td> ...
- CSS基础 盒子相关属性总结 padding+border
1.border当个属性: 作用 属性名 属性值 边框粗细 border-width 数字+px 边框样式 border-style solid实线.dashed虚线.dotted点线 边框颜色 bo ...
- Linux中ssh登陆慢的两种原因
useDNS配置导致登陆慢 如果ssh server的配置文件(通常是 /etc/ssh/sshd_config )中设置 useDNS yes ,可能会导致 ssh 登陆卡住几十秒.将该配置项设为 ...
- unittest_认识unittest(1)
unittest是python内置的单元测试框架,具备编写用例.组织用例.执行用例.输出报告等自动化框架的条件. 使用unittest前需要了解该框架的五个概念: 即test case,test su ...