题目地址:
https://www.acwing.com/problem/content/2439/
给定一个长度为
n
n
n的整数序列,初始时序列为
1
,
2
,
…
,
n
−
1
,
n
{1,2,…,n−1,n}
1,2,…,n−1,n。序列中的位置从左到右依次标号为
1
∼
n
1∼n
1∼n。我们用
[
l
,
r
]
[l,r]
[l,r]来表示从位置
l
l
l到位置
r
r
r之间(包括两端点)的所有数字构成的子序列。现在要对该序列进行
m
m
m次操作,每次操作选定一个子序列
[
l
,
r
]
[l,r]
[l,r],并将该子序列中的所有数字进行翻转。例如,对于现有序列1 3 2 4 6 5 7
,如果某次操作选定翻转子序列为
[
3
,
6
]
[3,6]
[3,6],那么经过这次操作后序列变为1 3 5 6 4 2 7
。请你求出经过
m
m
m次操作后的序列。
输入格式:
第一行包含两个整数
n
,
m
n,m
n,m。接下来
m
m
m行,每行包含两个整数
l
,
r
l,r
l,r,用来描述一次操作。
输出格式:
共一行,输出经过
m
m
m次操作后的序列。
数据范围:
1
≤
n
,
m
≤
1
0
5
1≤n,m≤10^5
1≤n,m≤105
1
≤
l
≤
r
≤
n
1≤l≤r≤n
1≤l≤r≤n
fhq treap的写法参考https://blog.csdn.net/qq_46105170/article/details/121119431。下面介绍splay tree(又叫做”伸展树“)的写法。
要翻转 [ l : r ] [l:r] [l:r]这段区间,则需要一个数据结构,可以将这个区间的数隔离出来,然后进行翻转。执行翻转的时候,只需要每个节点维护一个懒标记即可做到 O ( log n ) O(\log n) O(logn)的时间,对于怎么隔离出那个区间,fhq treap可以通过分裂的方式,而splay tree则是通过伸展的方式。
首先关于左旋右旋,可以参考https://blog.csdn.net/qq_46105170/article/details/118997891。而伸展树平衡的方式是,每次查询或添加数据的时候,都将该数据的节点通过旋转转到树根的位置。可以证明如果每次都进行这样的操作,可以使得每次操作期望时间复杂度是
O
(
log
n
)
O(\log n)
O(logn)的。而splay操作可以将指定的节点旋转到树根或者某个节点
p
p
p的儿子处,这里的
p
p
p一般指的就是树根(当然
p
p
p必须是指定的节点的某个祖宗,否则是没法转过去的)。例如,我们想把某个节点
x
x
x转到树根处。如果
x
x
x就是树根,那就不用转了;否则,如果
x
x
x的父亲是树根,则直接做一次单旋即可;否则
x
x
x的父亲
y
y
y和
y
y
y的父亲
z
z
z都非空。这里需要将
x
x
x向上旋两次,要按两种情况讨论:如果
x
x
x是
y
y
y的左儿子且
y
y
y也是
z
z
z的左儿子(或者
x
x
x是
y
y
y的右儿子且
y
y
y也是
z
z
z的右儿子),那么就先将
y
y
y旋上去,然后再将
x
x
x旋上去;如果
x
x
x是
y
y
y的左儿子且
y
y
y是
z
z
z的右儿子(或者
x
x
x是
y
y
y的右儿子且
y
y
y是
z
z
z的左儿子),那么直接将
x
x
x向上旋转两次。即
x
,
y
,
z
x,y,z
x,y,z如果是直线,则先转父亲再转自己;否则转两次自己。如图所示:
注意,只有这样的旋转才能证明每次操作期望时间复杂度
O
(
log
n
)
O(\log n)
O(logn)。
而对于伸展操作,只需要每次都将当前节点向上按上面的方式转上去,转到指定位置停止即可。
设原区间是
A
A
A,现在考虑实现插入和翻转
A
[
l
:
r
]
A[l:r]
A[l:r]操作:
1、插入就直接像普通BST那样插入,然后对插入的新节点做splay操作即可;
2、翻转
A
[
l
:
r
]
A[l:r]
A[l:r],先找到
A
[
l
−
1
]
A[l-1]
A[l−1]这个节点(当然这个节点可能不存在,所以我们需要加入两个哨兵节点,分别记为
0
0
0和
n
+
1
n+1
n+1号节点)splay到树根,然后将
A
[
r
+
1
]
A[r+1]
A[r+1]这个节点splay到树根下面,那么显然
A
[
r
+
1
]
A[r+1]
A[r+1]的左子树就是
A
[
l
:
r
]
A[l:r]
A[l:r],然后对其翻转即可,即直接打上懒标记。注意在查
A
[
l
−
1
]
A[l-1]
A[l−1]和
A
[
r
+
1
]
A[r+1]
A[r+1]的时候,每次向下之前都需要做pushdown操作。pushdown的做法是,交换左右子树,清空自己的懒标记,然后下传懒标记(即左右儿子打上懒标记,如果原来有懒标记则清除)。
代码如下:
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n, m;
struct Node {
int s[2], p, v;
int sz;
bool rev;
void init(int _v, int _p) { v = _v, p = _p, sz = 1; }
} tr[N];
int root, idx;
#define pushup(x) tr[x].sz = tr[tr[x].s[0]].sz + tr[tr[x].s[1]].sz + 1
// 下传懒标记
void pushdown(int x) {
// 如果自己有懒标记,则交换左右子树,下传懒标记,最后清空自己的懒标记
if (tr[x].rev) {
swap(tr[x].s[0], tr[x].s[1]);
tr[tr[x].s[0]].rev ^= 1;
tr[tr[x].s[1]].rev ^= 1;
tr[x].rev = 0;
}
}
// 将x向上转一次
void rotate(int x) {
int y = tr[x].p, z = tr[y].p, k = tr[y].s[1] == x;
tr[z].s[tr[z].s[1] == y] = x, tr[x].p = z;
tr[y].s[k] = tr[x].s[k ^ 1], tr[tr[x].s[k ^ 1]].p = y;
tr[x].s[k ^ 1] = y, tr[y].p = x;
pushup(y), pushup(x);
}
// 将x转到g的下面。如果g = 0,则转到树根。一般这里的g要么是0要么是树根
void splay(int x, int g) {
while (tr[x].p != g) {
int y = tr[x].p, z = tr[y].p;
// 如果三者不构成直线,则转父亲;否则转自己
if (z != g) rotate((tr[y].s[0] == x) ^ (tr[z].s[0] == y) ? x : y);
rotate(x);
}
if (!g) root = x;
}
void insert(int v) {
int u = root, p = 0;
while (u) p = u, u = tr[u].s[v > tr[u].v];
u = ++idx;
// 如果p不空,则插在p下面;如果p空,则说明树空,则直接new出节点v
if (p) tr[p].s[v > tr[p].v] = u;
tr[u].init(v, p);
splay(u, 0);
}
// 查树中中序遍历第k个数
int get_k(int k) {
int u = root;
while (u) {
// 下去查之前要先pushdown
pushdown(u);
if (tr[tr[u].s[0]].sz >= k) u = tr[u].s[0];
else if (tr[tr[u].s[0]].sz + 1 < k)
k -= tr[tr[u].s[0]].sz + 1, u = tr[u].s[1];
else return u;
}
return -1;
}
void dfs(int u) {
if (!u) return;
pushdown(u);
dfs(tr[u].s[0]);
if (tr[u].v >= 1 && tr[u].v <= n) printf("%d ", tr[u].v);
// 走到下一层之前要pushdown,保证答案正确
dfs(tr[u].s[1]);
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 0; i <= n + 1; i++) insert(i);
while (m--) {
int l, r;
scanf("%d%d", &l, &r);
// 要翻转原数组的[l : r],由于有哨兵节点,所以实际上是在
// 翻转[l + 1: r + 1],即要找第l和第r + 2个节点
l = get_k(l), r = get_k(r + 2);
// 将l转到树根,将r转到树根下面,则r的左子树即为要翻转的区间
splay(l, 0), splay(r, l);
// 翻转只需要打上懒标记即可
tr[tr[r].s[0]].rev ^= 1;
}
dfs(root);
}
每次操作时间复杂度 O ( log n ) O(\log n) O(logn)(splay两次加上一次打懒标记),空间 O ( n ) O(n) O(n)。