题解 P3391 【【模板】文艺平衡树】
UperFicial
2021-06-17 20:37:38
# $\texttt{FHQ-Treap yyds}$
### 前言
这题搞得我吐了![/px](https://cdn.luogu.com.cn/upload/pic/62246.png)
不过第一次写模板题题解还是很开心的呀![/cy](https://cdn.luogu.com.cn/upload/pic/62225.png)
感觉这道题题解质量都不高啊……
这里也会讲清楚此题题解并没有提及的一些重要问题。
看懂本篇题解,你需要了解 $\texttt{FHQ-Treap}$ 的前置芝士。
题目连接:[$\text{Link}$](https://www.luogu.com.cn/problem/P3391)
### 题意简述
给定一个长度为 $n$ 序列,第 $i$ 项初始为 $i$。
支持区间翻转操作,即翻转区间 $[l,r]$。
输出 $m$ 次翻转操作后的序列。
$1\le n,m\le 10^5$。
### 题目解析
这道题我是用的 $\texttt{FHQ-Treap}$,毕竟很好写。
既然都用 $\texttt{FHQ-Treap}$,那么大概率会有的一个想法就是:把 $[l,r]$ 这段区间在这棵平衡树上裂出来,然后再搞。
先别着急怎么裂,假设我们已经裂出来这棵树了,那么,这棵树里每个子节点的权值 $v$ 肯定有 $l\le v\le r$。
假设我们分裂出如下的一棵根节点为 $u$ 的一棵树(图中字母为编号)。
![](https://cdn.luogu.com.cn/upload/image_hosting/1jndp39v.png)
如果不考虑翻转的情况下,这棵树是要按照**中序遍历**来输出,即 $\texttt{c-a-d-u-b}$
但如果翻转了呢?就把中序遍历给翻过来,也就是 $\texttt{b-u-d-a-c}$。
考虑我们到底肝了个什么事。
发现,其实就是把这棵树里,每一个节点的两个左右孩子调换后,得到的新树再进行中序遍历,所以上面的这棵树就变成了下面这个样子:
![](https://cdn.luogu.com.cn/upload/image_hosting/z7b670bt.png)
于是,我们一个初步的思路就是:分裂出对应的树后,从根开始,每一层的每一个节点都将两个儿子调换。
但是考虑我们这样的复杂度是多少,我们要进行分裂出来的树的节点的个数次调换,那么复杂度就是 $\mathcal{O}(r-l+1)$,最坏情况 $\mathcal{O}(n)$,跟朴素暴力一个德行,直接上天。
考虑优化,引入线段树那里的懒标记的思想。
还以这棵树为例,前提这棵树是分裂好的了:
![](https://cdn.luogu.com.cn/upload/image_hosting/1jndp39v.png)
现在想对这棵树进行翻转,我们可以先不翻转,而是记上要翻转的名义,那么在哪记?当然是这棵树的根节点也就是 $u$ 了。这里的标记是需要将原有的懒标记**取反**的,因为假设这同一个区间翻转了两次,就相当于啥都没干,取反两次翻转的名义也就没了。这样既省了时间复杂度,又省了代码复杂度。
现在问题又来了,我们光知道哪个树需要翻转,但是我们并没有真正地更改儿子,输出的时候怎么办。
这也是懒标记的精髓,用它的时候再更新。当我们要进行分裂、合并时,对于目前的节点 $u$,如果它有翻转的名义,也就是被标有懒标记了,那么,它的两个儿子其实就要调换了,而点 $u$ 的懒标记也就随之消失,因为已经它的儿子已经调换了。
而它的两个儿子调换之后,不光它们要调换,它们的后代也都要调换,所以它们也会承接父亲的懒标记,注意,这里的承接是指的取反,也跟上面讲述的同理,是为了节省翻多次的时间,这里不再赘述。
故到此,我们就实现了标记下传:
```cpp
inline void push_down(int u)
{
tag[u]=false;
Swap(ch[u][0],ch[u][1]);
tag[ch[u][0]]^=1;
tag[ch[u][1]]^=1;
return;
}
```
之后还有一个问题:在分裂或合并时什么时候下传标记。
这个问题我想了半天,因为我太 $\texttt{naive}$ 了。因为我们在分裂的时候,对于一个节点 $u$,它的两个儿子就可能会改变,这样一来,假设我们在分裂之后来下传,可能会传到两个假儿子,就导致了 [$\texttt{\color{red}WA}$](https://www.luogu.com.cn/record/51863995)。~~没人发现这个可以点吗~~
所以,只有我们在分裂之前下传,才能传到真儿子中。当然,合并也是同理的。
这样一来,又有一个问题,我们在进行 `push_down` 时,会调换两个儿子,那这样怎么能保证 $\texttt{treap}$ 的性质?
其实,这道题,它跟权值是没有关系的,我们只关心每个子树内节点的顺序如何,我们维护的其实就是翻转后的中序遍历(个人理解)。
所以说我们回到本文开头笔者未解答的问题,怎么分裂出那棵需要翻转的子树?
由于我很 $\texttt{naive}$,一直都是按照权值来分裂,然后一直爆蛋。因为当你下传标记的时候,这个树就已经满足不了**二叉搜索树的性质**了。
~~那咋办,分裂不了啥都玩完了了,洗洗睡吧。~~![/youl](https://cdn.luogu.com.cn/upload/pic/69020.png)
既然我们不能按权值分,我们可以按照子树的大小来分!也就是 $\texttt{FHQ-Treap}$ 另一种经典分裂途径。
这样分裂,简单来说,就是分出权值的效果,但又不需要权值。
然后假设我们有如下的一棵树。
![](https://cdn.luogu.com.cn/upload/image_hosting/h5tjccjf.png)
圈内代表节点编号,红字代表节点的值。
它显然不满足 $\texttt{BST}$ 的性质,因为我们维护的是中序遍历。
那么目前,此序列即为 $\texttt{2-3-5-1-4}$ 这是按照对此树中序遍历得来的。
有一个很显然的性质:对于目前序列一个连续的区间,在这棵树中所对应的节点也是相连的。
那么也很显然,我们只要分裂两次就能分裂出我们先要的区间。
那么分裂就显而易见了,对于翻转 $[l,r]$,我们先把 $[1,l-1]$ 这个区间分裂,也就是大小为 $l-1$ 的子树分裂,然后对于这个 $[l,n]$ 的这个树,我们分裂出大小 $r-l+1$ 的一棵树。这个只要你知道啥是按大小分裂都能想清楚吧。
之后就是输出了,当然就是按照中序遍历输出,但还有一个问题,有些节点可能标记没下传下去,遍历的时候也要下传。
呼,讲完了,此生最长的一片题解了![/cy](https://cdn.luogu.com.cn/upload/pic/62225.png)
[$code$](https://paste.ubuntu.com/p/bwpzbrf6HG/)
时间复杂度:$\mathcal{O}(n\log n)$,树的深度约为 $\log n$。
空间复杂度:$\mathcal{O}(n)$。
求赞![/kel](https://cdn.luogu.com.cn/upload/pic/62226.png)
$$\texttt{The End.by UF}$$