题解:Splay
一、题目分析
本题给定一个初始为({1, 2, \cdots, n - 1, n})的整数序列,对其进行(m)次操作,每次操作选定一个子序列([l, r]),将该子序列中的所有数字进行翻转,要求输出经过(m)次操作后的序列。
二、解题思路
本题使用Splay树来解决,Splay树是一种自平衡二叉搜索树,通过旋转操作保持树的平衡,同时利用其性质来实现区间翻转。
(一)Splay树节点定义
struct Node
{
int s[2], p, v;
int size, flag;
void init(int _v, int _p)
{
v = _v, p = _p;
size = 1;
}
}tr[N];
定义节点结构体Node
,包含左右子节点指针s[2]
、父节点指针p
、节点值v
、子树大小size
以及翻转标记flag
。init
函数用于初始化节点。
(二)Splay树基本操作函数
- 更新节点大小函数
pushup
:
void pushup(int x)
{
tr[x].size = tr[tr[x].s[0]].size + tr[tr[x].s[1]].size + 1;
}
根据左右子树的大小更新当前节点的子树大小。
- 下传翻转标记函数
pushdown
:
void pushdown(int x)
{
if (tr[x].flag)
{
swap(tr[x].s[0], tr[x].s[1]);
tr[tr[x].s[0]].flag ^= 1;
tr[tr[x].s[1]].flag ^= 1;
tr[x].flag = 0;
}
}
若当前节点有翻转标记,交换其左右子树,并将翻转标记下传给左右子节点,同时清除当前节点的标记。
- 旋转函数
rotate
:
void rotate(int x)
{
int y = tr[x].p, z = tr[y].p;
int k = tr[y].s[1] == x; // k=0表示x是y的左儿子;k=1表示x是y的右儿子
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
进行旋转操作,根据x
是其父节点y
的左儿子还是右儿子,进行相应的旋转,并更新节点大小。
- Splay操作函数
splay
:
void splay(int x, int k)
{
while (tr[x].p != k)
{
int y = tr[x].p, z = tr[y].p;
if (z != k)
if ((tr[y].s[1] == x) ^ (tr[z].s[1] == y)) rotate(x);
else rotate(y);
rotate(x);
}
if (!k) root = x;
}
将节点x
通过旋转操作调整到节点k
的子节点位置(若k = 0
,则调整到根节点)。
(三)插入函数insert
void insert(int v)
{
int u = root, p = 0;
while (u) p = u, u = tr[u].s[v > tr[u].v];
u = ++ idx;
if (p) tr[p].s[v > tr[p].v] = u;
tr[u].init(v, p);
splay(u, 0);
}
向Splay树中插入值为v
的节点,先找到合适的插入位置,然后插入节点并将其Splay到根节点。
(四)获取第k
小元素的节点函数get_k
int get_k(int k)
{
int u = root;
while (true)
{
pushdown(u);
if (tr[tr[u].s[0]].size >= k) u = tr[u].s[0];
else if (tr[tr[u].s[0]].size + 1 == k) return u;
else k -= tr[tr[u].s[0]].size + 1, u = tr[u].s[1];
}
return -1;
}
在Splay树中找到第k
小的元素对应的节点,通过比较左子树大小和k
的值,在树中进行搜索。
(五)输出函数output
void output(int u)
{
pushdown(u);
if (tr[u].s[0]) output(tr[u].s[0]);
if (tr[u].v >= 1 && tr[u].v <= n) printf("%d ", tr[u].v);
if (tr[u].s[1]) output(tr[u].s[1]);
}
中序遍历Splay树,输出节点值在(1)到(n)之间的元素,在遍历前先下传翻转标记。
(六)主函数main
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 = get_k(l), r = get_k(r + 2);
splay(l, 0), splay(r, l);
tr[tr[r].s[0]].flag ^= 1;
}
output(root);
return 0;
}
- 读取序列长度
n
和操作次数m
。 - 向Splay树中插入(0)到(n + 1)的节点,其中(0)和(n + 1)作为哨兵节点,方便处理边界情况。
- 对于每次操作,读取子序列的左右端点
l
和r
,通过get_k
找到对应的节点,然后将左端点节点Splay到根节点,右端点节点Splay到左端点节点的右子节点位置,对右端点节点的左子树进行翻转(通过翻转标记实现)。 - 最后通过中序遍历输出经过操作后的序列。
四、时间复杂度和空间复杂度
(一)时间复杂度
- 插入操作:每次插入时间复杂度为(O(\log n)),初始化插入(n + 2)个节点,时间复杂度为(O(n\log n))。
- 每次操作:获取节点、Splay操作和标记翻转时间复杂度均为(O(\log n)),(m)次操作总时间复杂度为(O(m\log n))。
- 输出操作:中序遍历时间复杂度为(O(n))。
- 因此,总的时间复杂度为(O((n + m)\log n))。
(二)空间复杂度
代码中使用了数组模拟的Splay树,最多存储(n + 2)个节点,所以空间复杂度为(O(n))。