题解
离上一次写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 l−1 大的翻到根节点,再把第 r + 1 r+1 r+1 大的节点翻到根节点的右儿子的位置,将 l 、 r l、r l、r 所在的子树放到同一个父节点下,最后交换这两棵子树,再中序遍历输出(左中右),就是我们翻转一次区间要的答案了
由于每次翻转需要第 l − 1 、 r + 1 l-1、r+1 l−1、r+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;
}