“勇而无畏之人,在于面对不可掘凿的基土之时,开辟直达深渊的幽径。”
动态开点线段树
这种东西其实在很久以前就见识过了,不过那是一种动态开店线段树的变种,名为主席树,也就是可持久化线段树。但实际上,动态开点也可以用在普通线段树上。
首先,我们知道,一棵完全线段树的结点个数为 $4n$ ,那么其使用的空间复杂度也就是 $\mathcal O(4n)$ ,如果 $n$ 大的离谱的话,就可能会存在 $\textcolor{Blue}{\text{Memory Limit Exceeded}}$ 的情况,而我们又会发现,如果 $\mathcal O(4n)$ 的空间中,并不是每一个结点都被用到了的话,我们就没有必要去建立,而在需要使用的时候再去动态开点,这样的话,空间复杂度甚至能够小到 $\mathcal O(\log n)$ (只开了一条链),并达到了相同的效果(唯一的缺点就是有些难写,有些难调)。
再次重复,因为是动态开点,所以父子关系不再满足二倍,所以直接存储子结点编号。
示例写法
AC Code
1 | const int MAXN=1e5+10; |
然后你就会发现在这种近乎开满的情况下,动态开点和完全线段树在时间,空间,码量上都差别不大,甚至还要难写一点。但是,如果是对于一些比较特殊的区间问题,动态开点将会派上大用场。
线段树合并
顾名思义,把两棵线段树合并在一起,和 $\text{Fhq-Treap}$ 不同,这里的合并,仅仅指权值合并,表示区间是相同的。即对于区间 $[l,r]$ ,合并之后的权值就是 $Tr[p1].val+Tr[p2].val$ ,但这两棵线段树表示的依然是 $[l,r]$ 区间。
结果会发现,如果是完全线段树的话,我们递归合并不如直接扫一遍线段树数组来合并。所以说,线段树合并在完全线段树的运用下就是毫无作用的,这也是为什么在这篇文章的前面我们会介绍动态开点线段树,因为线段树合并一般都用在动态开点上(且用于值域类线段树),时间复杂度在 $\log$ 级别的。
离线破结式合并
假设现在有两棵动态开点线段树,记为 $Tr_a,Tr_b$ ,离线破结式合并的优点在于不需要申请额外的空间,直接把 $Tr_b$ 粘贴到 $Tr_a$ 上,但是,这样的合并会破坏掉 $Tr_b$ 的结构,从而影响之后的操作,仅支持离线操作。
1 | int merge(int p1,int p2,int l,int r) |
在线动态开点式合并
顾名思义,对于合并 $Tr_a,Tr_b$ ,我们视作是新建一棵线段树 $Tr_c$ ,但是记录的信息就是 $Tr_a$ 和 $Tr_b$ 的信息之和。这样做的优点就是可以支持在线操作和可持久化(回溯操作),而缺点也是显然的,那就是空间爆炸。
1 | int merge(int p1,int p2,int l,int r) |
例题
P3605
其实我觉得这道题更适合练手一些。毕竟不涉及其它操作,而且线段树存储的参数也只有一个。
需要注意的是,动态开点线段树一般会搭配离散化来使用,而且其空间一般而言都需要到 $\mathcal O(n\log n)$ 甚至更多,一般而言,就开 MAXN<<4
或者 MAXN<<5
就差不多。否则会 $\textcolor{darkblue}{\text{Time Limit Exceeded}}$ 和 $\textcolor{Purple}{\text{Runtime Error}}$ 双倍快乐。
AC Code
1 | const int MAXN=1e5+10; |
CF600E
对于每一个结点建立一棵动态开点权值线段树,然后维护当前结点(肯定只有一种颜色)的答案和,然后从叶结点向上合并并更新答案即可。
AC Code
1 | const int MAXN=1e5+10,MAXTREE=3e6+10; |
雨天的尾巴
名副其实的线段树合并模板题。
单独写了一篇博客
P3521
有些思考难度,至少我没有做出来。
因为读入是按照递归读入的,所以也考虑在读入的时候处理子树,并最后递归得到答案。考虑当前子树对整棵树的影响,记录 $spa$ 表示当前左右子树不交换得到的逆序对数,$spb$ 表示当前子树交换的逆序对数,最后统计较小值即可。
AC Code
1 | const int MAXN=1e6+10; |
线段树分裂
顾名思义,当维护整棵线段树十分困难的时候,考虑拆分成多棵线段树分别维护,形式上线段树合并的逆操作。
可以联想 $\text{Fhq-Treap}$ 的写法中也有 $\text{merge}$ 和 $\text{split}$ 操作,也可以大致猜到线段树分裂的方式。
子树大小分裂 / 排名分裂
一般而言,线段树分裂使用的范围也是动态开点值域线段树,所以呢,分裂形式与 $\text{Fhq-Treap}$ 一致,分成 $[1,k-1][k,N]$ 两部分,俗称按权值分裂,或者排名分裂。
首先说按排名分裂,即分裂出第 $k$ 个之后的数(可重),这里设原来的线段树为 $p_1$ ,分裂出的线段树为 $p_2$ 。
1 | void split(int p1,int &p2,int k) |
这个思路类似于查找区间第 $k$ 小值,可以类比理解。
例题
P5494
一个一个分析操作,第一个操作,就是按权值分裂两次并合并第一三个,对于如何把排名分裂转换为权值分裂,我们可以用到操作 $3$ ,首先查找 $[1,x-1]$ 的个数,就知道了 $x$ 的排名了,同理。
注意:不!要!把!下!标!打!错!了!
AC Code
1 | const int MAXN=1e6+10; |
P2824
这道题有一个简化版 $\text{CF558E A Simple Task}$ ,但如果用线段树分裂的话,就会变成加强版,这个我们之后再提。