【笔记】线段树合并 2025.3.5
参考资料
线段树合并:从入门到精通 - 题单 - 洛谷 | 计算机科学教育新生态
【笔记】数据结构专题 2023.8.5 - caijianhong - 博客园
【笔记】数据结构选讲 2025.2.10 - caijianhong - 博客园
数据结构专题-学习笔记:线段树合并 - Plozia - 博客园
线段树合并:从入门到放弃 - 洛谷专栏
题解 P4770 【NOI2018 你的名字】 - 洛谷专栏
《浅谈数据结构的合并与分裂》黄洛天
动态开点线段树
如果全局只有 \(O(1)\) 棵线段树,我们可以将线段树节点尽可能连续标号,使非叶子节点 \(x\) 的左右儿子分别为 \(2x, 2x+1\),这样值域为 \([1, n]\) 的线段树只需要最多 \(4n\) 个节点就能存下来。
但是如果全局有 \(O(n)\) 棵线段树,而且每棵线段树不一定是满的(可能有一些节点自始至终都没有修改过,值为空),这个时候再对每棵线段树开 \(4n\) 个节点显然不合适。
我们引入动态开点线段树,一开始没有任何点,每次尝试访问一个节点时,如果它是空的,我们从未访问过它,则我们新建一个节点并记录相应的信息。由于节点的儿子可能是乱的,我们需要额外的空间记录每个节点的两个儿子的编号,或者将它们记为空。可以发现,由于正常线段树上每次操作的复杂度为 \(O(\log n)\),动态开点线段树上的每次操作新建的节点也不会超过 \(O(\log n)\),所以假如操作了 \(m\) 次,那么节点数量最多为 \(O(\min(n, m\log n))\)。并且这样操作的话就能进一步扩大值域,即使 \(n=10^9\),只要 \(m\) 在合理的范围内,算法就能运行。
线段树合并(1)
如果有两棵不带懒标记的动态开点线段树,我们希望把它们合并。什么是合并?
例题 1
维护 \(n\) 个数组 \(a_1, a_2, \cdots, a_n\),每个 \(a_i\) 都是长度为 \(n\) 的数组,初始全为零。进行 \(m\) 次操作,每次操作为以下三种之一:
- 给定 \(i, j, v\),使 \(a_{i, j}\) 加上 \(v\)。
- 给定 \(i, j\),对所有 \(1\leq k\leq n\) 进行 \(a'_{i, k}=a_{i, k}+a_{j, k}\) 的操作,并将 \(a_j\) 标记为删除,以后不再访问 \(a_j\)。
- 给定 \(i, l, r\),查询 \(\sum_{j=l}^ra_{i, j}\)。
强制在线。\(n, m\leq 2\times 10^5\)。
如果没有 2 操作,我们开 \(O(n)\) 棵动态开点线段树就解决了。
如果有 2 操作,我们尝试线段树合并,将 \(a_i\) 和 \(a_j\) 所对应的线段树合并到一起,新的线段树的信息要满足 \(a'_{i, k}=a_{i, k}+a_{j, k}\)。这个 \(a_i\) 实际上就是它对应的线段树的所有叶子的中序遍历的结果,如果 \(a_i\) 有 \(x\) 个非零值,\(a_j\) 有 \(y\) 个非零值,则新的 \(a_i'\) 最多有 \(x+y\) 个非零值,这只与 \(x, y\) 相关。
而我们又知道,如果我们用动态开点线段树,线段树有 \(x\) 片非空的叶子,则其节点个数可以不超过 \(x\log n\)。我们的尝试就是将两边节点个数分别为 \(x\log n, y\log n\) 的线段树合并为 \((x+y)\log n\) 个节点数的线段树,同时维护好 \(a_i\)。
递归的进行这个过程,设合并两棵线段树,两边分别走到 \(p, q\) 节点:
- 如果 \(p\) 是空的,则返回 \(q\)。
- 如果 \(q\) 是空的,则返回 \(p\)。
- 如果 \(p, q\) 都是叶子,则将两边的 \(a_{i, k},a_{j, k}\) 加起来,假如加到 \(p\) 上,就返回 \(p\)。
- 否则 \(p, q\) 都不是叶子,合并 \(p\) 的左儿子和 \(q\) 的左儿子,作为新的 \(p\) 的左儿子;合并 \(p\) 的右儿子和 \(q\) 的右儿子,作为新的 \(p\) 的右儿子。对 \(p\) 做一次 pushup,返回 \(p\)。
(这里画两棵小线段树模拟一下,\(a_i=\{1, 0, 3, 0\}, a_j=\{0, 6, 7, 0\}\))
可以发现:
- 每次合并节点数不会变多只会变少。
- 每次合并遍历到的点只有被删除的点和它们的儿子,也就是遍历的点数比上被删除的点数为常数。
所以可以认为,线段树合并的复杂度为初始没有合并时的总点数。这个值刚好就是 \(O(m\log n)\)。所以复杂度为 \(O(m\log n)\)。至此就解决了这个问题。
线段树合并(2)
例题 2(P4556 [Vani有约会] 雨天的尾巴 /【模板】线段树合并 - 洛谷)
首先村落里的一共有 \(n\) 座房屋,并形成一个树状结构。然后救济粮分 \(m\) 次发放,每次选择两个房屋 \((x, y)\),然后对于 \(x\) 到 \(y\) 的路径上(含 \(x\) 和 \(y\))每座房子里发放一袋 \(z\) 类型的救济粮。
然后深绘里想知道,当所有的救济粮发放完毕后,每座房子里存放的最多的是哪种救济粮。
对于 \(100\%\) 测试数据,保证 \(1 \leq n, m \leq 10^5\),\(1 \leq a,b,x,y \leq n\),\(1 \leq z \leq 10^5\)。
这个问题太难了,竟然要对树上的一条链进行操作。不过我们注意到信息是可以差分的,假如我们做树上前缀和和树上差分,那就将 \(x\) 到 \(y\) 的链拆成 \(x\) 到根的链 \(+\) \(y\) 到根的链 \(-\) \(\text{LCA(x, y)}\) 到根的链 \(-\) \(fa[\text{LCA}(x, y)]\) 到根的链。
在 \(n\) 个节点上各开一棵线段树,如果 \(x\) 到根的链上要发一袋 \(z\) 类救济粮,就在 \(x\) 点上的线段树的 \(z\) 位置 \(+1\)。最后 dfs 整棵树,从下到上,dfs 完自己的所有儿子后将它们的线段树与自己的合并,这样自己的线段树就留有整棵子树的信息。此时立即查询全局最大值并记下来作为答案。复杂度 \(O(m\log n)\)。
线段树合并(3)
例题 3(P3605 [USACO17JAN] Promotion Counting P - 洛谷)
有一棵以 \(1\) 为根的有根树,每个点有点权 \(p_i\)。对于每个节点 \(i\) 求出它子树中有多少个 \(j\) 满足 \(p_j>p_i\)。
在每个节点上开一棵线段树,\(i\) 节点上线段树往 \(p_i\) 的位置插入 \(1\)。然后将自己子树内的线段树合并上来,做对位加的合并,然后查询 \(>p_i\) 的部分的和即可。建议离散化。
线段树合并(4)
例题 4(CF600E Lomsat gelral - 洛谷)
- 有一棵 \(n\) 个结点的以 1 号结点为根的有根树。
- 每个结点都有一个颜色,颜色是以编号表示的, \(i\) 号结点的颜色编号为 \(c_i\)。
- 如果一种颜色在以 \(x\) 为根的子树内出现次数最多,称其在以 \(x\) 为根的子树中占主导地位。显然,同一子树中可能有多种颜色占主导地位。
- 你的任务是对于每一个 \(i\in[1,n]\),求出以 \(i\) 为根的子树中,占主导地位的颜色的编号和。
- \(c_i\leq n\leq 10^5\)
与上一题一样,但是操作变成查询整棵树的最大值,这也是可以合并的。
线段树合并(5)
例题 5(P3521 [POI 2011] ROT-Tree Rotations - 洛谷)
给定一颗有 \(n\) 个叶节点的二叉树。每个叶节点都有一个权值 \(p_i\)(注意,根不是叶节点),所有叶节点的权值构成了一个 \(1 \sim n\) 的排列。
对于这棵二叉树的任何一个结点,保证其要么是叶节点,要么左右两个孩子都存在。
现在你可以任选一些节点,交换这些节点的左右子树。
在最终的树上,按照先序遍历遍历整棵树并依次写下遇到的叶结点的权值构成一个长度为 \(n\) 的排列,你需要最小化这个排列的逆序对数。
对于 \(100\%\) 的数据,保证 \(2 \leq n \leq 2 \times 10^5\), \(0 \leq x \leq n\),所有叶节点的权值是一个 \(1 \sim n\) 的排列。
交换左右子树会使左右子树之间产生的逆序对发生变化,而左右子树各自内部产生的逆序对不会发生变化。
可以计算 \(u, v\) 表示左 / 右子树先遍历产生的逆序对数。尝试线段树合并。
考虑如果有一棵完整的线段树,其中每片叶子有一个颜色黑或白,现在要计算所有黑叶子前面有多少片白叶子。我们在线段树上分治,强制要求用黑去查询白。假如当前到了线段树某个节点上,将答案分成:黑白都在左子树、黑白都在右子树、黑在右子树白在左子树,共三种情况,前两种自己递归下去,第三种可以维护每个节点子树内黑叶子和白叶子的数量,做一个乘法即可。以及有一些剪枝:如果子树全是黑的或者全是白的,就不用向下递归了,只有子树中又有黑又有白的时候尝试计算。
将刚才这个过程重新理解成线段树合并即可,可以发现需要计算的节点恰好是“又有黑又有白”的,刚好是两棵线段树的重叠部分。
线段树合并(6)
例题 6(CF208E Blood Cousins - 洛谷 或 P5384 [Cnoi2019] 雪松果树 - 洛谷)
有一个家族关系森林,描述了 \(n\)(\(1\leq n\leq 10 ^ 5\))人的家庭关系,成员编号为 \(1\) 到 \(n\) 。
如果 \(a\) 是 \(b\) 的父亲,那么称 \(a\) 为 \(b\) 的 \(1\) 级祖先;如果 \(b\) 有一个 \(1\) 级祖先,\(a\) 是 \(b\) 的 \(1\) 级祖先的 \(k-1\) 级祖先,那么称 \(a\) 为 \(b\) 的 \(k\) 级祖先。
家庭关系保证是一棵森林,树中的每个人都至多有一个父母,且自己不会是自己的祖先。
如果存在一个人 \(z\) ,是两个人 \(a\) 和 \(b\) 共同的 \(p\) 级祖先:那么称 \(a\) 和 \(b\) 为 \(p\) 级表亲。
\(m\)(\(1\leq m\leq 10 ^ 5\))次询问,每次询问给出一对整数 \(v\) 和 \(p\),求编号为 \(v\) 的人有多少个 \(p\) 级表亲。
相当于问 \(v\) 的 \(p\) 级祖先的子树中有多少个点到它的距离为 \(p\)。考虑离线,做 1 次 dfs 1 次长链剖分,复杂度线性。先解决树上 \(k\) 级祖先问题,然后考虑线段树合并,每个点开一棵线段树,在 \(i\) 号点的线段树上在 \(dep_i\) 位置插入 \(1\),然后将自己的子树中的线段树合并上来,做对位加,最后查询 \(dep_i+p\) 位置上的值。
当然你发现将树拍成 dfs 序后,这题就能写成二维数点问题,可以写树状数组扫描线直接解决,这样就更好写。总的来说这题做法很多,线段树合并反而是最劣的一种做法。
线段树合并(7)
例题 7(P3899 [湖南集训] 更为厉害 - 洛谷)
设 \(\text T\) 为一棵有根树,我们做如下的定义:
- 设 \(a\) 和 \(b\) 为 \(\text T\) 中的两个不同节点。如果 \(a\) 是 \(b\) 的祖先,那么称“\(a\) 比 \(b\) 更为厉害”。
- 设 \(a\) 和 \(b\) 为 \(\text T\) 中的两个不同节点。如果 \(a\) 与 \(b\) 在树上的距离不超过某个给定常数 \(x\),那么称“ \(a\) 与 \(b\) 彼此彼此”。
给定一棵 \(n\) 个节点的有根树 \(\text T\),节点的编号为 \(1\) 到 \(n\),根节点为 \(1\) 号节点。
你需要回答 \(q\) 个询问,询问给定两个整数 \(p\) 和 \(k\),问有多少个有序三元组 \((a,b,c)\) 满足:
- \(a,b,c\) 为 \(\text T\) 中三个不同的点,且 \(a\) 为 \(p\) 号节点;
- \(a\) 和 \(b\) 都比 \(c\) 更为厉害;
- \(a\) 和 \(b\) 彼此彼此。这里彼此彼此中的常数为给定的 \(k\)。
\(n, q\leq 300000\)。
当然是先确定 \(c\) 再去找 \(b\)。分成两种情况:
- \(b\) 是 \(a\) 的祖先,这一部分的贡献为 \(\min(dep_a - 1, k)(siz_a-1)\)。
- \(a\) 是 \(b\) 的祖先,这一部分在选定 \(c\) 后贡献为 \(\min(dep_c-dep_a-1, k)\),需要将 \(\min\) 拆开:
- \(dep_c-dep_a-1\leq k\),这时 \(c\) 就有一个深度限制,然后 \(b\) 的贡献就和 \(dep_c\) 有关。相当于要计算 \(c\) 在 \(a\) 的子树中,\(c\) 的深度 \(\leq dep_a+k+1\),求有多少个 \(c\) 以及它们的 \(dep_c\) 的总和(这样才能计算 \(dep_c-dep_a-1\))。
- \(dep_c-dep_a-1> k\),相当于要计算 \(c\) 在 \(a\) 的子树中,\(c\) 的深度 \(> dep_a+k+1\),求有多少个 \(c\),然后乘上 \(k\) 作为答案贡献。
这些问题可以用长链剖分解决,复杂度线性。这些问题用线段树合并解决,和上一题差不多,但是单点查询改成区间求和,时间复杂度 \(O(n\log n)\)。当然你发现这能写成二维数点问题,可以写树状数组扫描线直接解决,这样就更好写。
线段树合并(8)
例题 8(CF1009F Dominant Indices - 洛谷)
给定一棵以 1 为根,\(n\) 个节点的树。设 \(d(u, k)\) 为 \(u\) 子树中到 \(u\) 距离为 \(x\) 的节点数。
对于每个点,求一个最小的 \(k\),使得 \(d(u, k)\) 最大。
显然,这是一道长链剖分题,使用长链剖分即可,复杂度线性。显然,这是一道线段树合并题,和前面的没有区别,复杂度 \(O(n\log n)\)。
线段树合并(9)
例题 9(CF570D Tree Requests - 洛谷)
给定一个以 1 为根的 \(n\) 个结点的树,每个点上有一个字母(
a
-z
),每个点的深度定义为该节点到 1 号结点路径上的点数。每次询问 \(a\),\(b\) 查询以 \(a\) 为根的子树内深度为 \(b\) 的结点上的字母重新排列之后是否能构成回文串。
判定一些字符能否重新排列为回文串,只需要出现次数为奇数的字符不超过一种即可。由于字符集太小,将每种字符的出现次数的奇偶性压缩成一个 int 即可,接下来就转化为了一道长链剖分题,使用长链剖分即可,复杂度线性。显然,这是一道线段树合并题,和前面的没有区别,复杂度 \(O(n\log n)\)。
线段树合并(10)
例题 10(CF246E Blood Cousins Return - 洛谷)
给定一片森林,每个点有一个名字(字符串)。每次查询一个节点的子树中离他距离为 \(k\) 的点中有多少种不同的名字。\(n, m\leq 10^5\)。
将所有名字离散化后有两种方法:第一种是将它写为平面线段数颜色,将平面一行一行地拍平成线段,然后做区间数颜色,复杂度 \(O(n\log n)\)。第二种是直接做线段树合并,在叶子节点上做平衡树(std::set
即可)的启发式合并,复杂度 \(O(n\log^2n)\)。
还有一种想法:对于每个点,找出与它深度相同的,dfs 序在他前面的,颜色也与它相同的点(这个可以用 bfs 序 \(O(n)\) 找出),求出这两个点的 LCA,然后在这个点本身的这个深度上打一个 \(+1\) 标记,在 LCA 处的这个深度上打一个 \(-1\) 标记,表示在 LCA 处颜色出现了重复,减去自己,用前面与它颜色相同的点为这个颜色的代表进行计算。这和第一种做法是差不多的,但是更好的利用了树的性质,做到了线性(假设 LCA 是线性的)。
例题 6-10 总结
线段树合并如果上树的话,很多情况下可以改写为多维偏序或者长链剖分问题。如果能将其它维用线段树解决,且信息支持对位合并,就能无代价的将这个问题转化到子树上。但如果另一维是深度而且信息足够简单,建议直接写长链剖分;如果空间不足且支持离线,建议使用树上启发式合并(dsu on tree)或后文的线段树合并线性空间优化;还有最后一种方法,将树拍成 dfs 序,子树变为 dfs 序区间,将问题升高一维,用其它办法(例如可持久化线段树、K-D Tree、cdq 分治)等重新解决,但是这样会丧失潜在的优化的空间(增加了一个“自由度”)。
无法 pushup 的线段树合并
有的时候线段树是不支持 pushup 的。一个极端的例子是每个节点上是一棵平衡树,正常的修改都没有进行 pushup,那么线段树合并的时候就只能将两棵平衡树对位合并,这时变成平衡树合并复杂度分析,有另外一套算法。
带懒标记的线段树合并
有的时候如果有区间操作,则很有可能有懒标记,而每次下方懒标记的时候都有可能新建点,这样的情况下还能保证复杂度正确吗?
如果我们时刻保证:每个点要么没有儿子,要么有两个儿子,那么每次下放标记都不会新建节点。我们只需要关心一个没有儿子的节点怎么和有儿子的节点合并。强行下方没有儿子的节点的标记肯定是不行的,由于这个没有儿子的节点,它所代表的区间往往有一个统一的性质,该性质由懒标记提供,我们将这个标记直接打到有儿子的节点上就好了。这样的操作可能对懒标记的性质要求较高,比如要求懒标记之间可以交换。
“可持久化”线段树合并
例题 3(P4770 [NOI2018] 你的名字 - 洛谷 后面的线段树合并部分)
维护 \(n\) 个数组 \(a_1, a_2, \cdots, a_n\),每个 \(a_i\) 都是长度为 \(n\) 的数组,初始全为零。进行 \(m\) 次操作,每次操作为以下三种之一:
- 给定 \(i, j, v\),使 \(a_{i, j}\) 加上 \(v\)。
- 给定 \(i, j\),对所有 \(1\leq k\leq n\) 进行 \(a'_{i, k}=a_{i, k}+a_{j, k}\) 的操作,并保证此后不再对 \(a_j\) 进行操作 1, 2。
- 给定 \(i, l, r\),查询 \(\sum_{j=l}^ra_{i, j}\)。
强制在线。\(n, m\leq 2\times 10^5\)。
首先需要思考为什么会有这样的问题,原来的算法难道不适用吗?确实不适用,虽然看起来 \(a_j\) 的线段树节点是一点也没有动过,但是如果下次操作修改了 \(a_j\) 在 \(a_i\) 中的部分,\(a_j\) 对应的信息就错了。
正确的做法是进行可持久化,这里的持久化操作比较简单,我们每次合并的时候不是合并到 \(p\) 上,而是新建一个节点,将合并后的信息放在 \(z\) 上,原来的节点的信息保持不动,这样以后就能去查询原来的节点的信息了。
可以发现经过这个持久化操作,复杂度不会发生变化。
注意你需要保证“此后不再对 \(a_j\) 进行操作 1, 2”,尤其是不能进行操作 2,不然无法使用原先的复杂度分析(这时变成真正的“可持久化线段树合并”,疑似图灵奖)。
线性空间线段树合并
例题(# 9605. 新生舞会 - Problem - QOJ.ac)
线段树合并,但是需要线性空间
引理 1:经过 \(m\) 次操作的动态开点线段树有 \(O(m(\log n-\log m))\) 个节点。
考虑对线段树节点分类,\(dep\leq\log m\) 的部分最多 \(O(m)\) 个节点,\(dep>\log m\) 的部分最多 \(O(m)\) 条链,每条链长度为 \(\log n-\log m\),所以总节点数量为 \(O(m(\log n-\log m))\)。可以发现这个上界实际上还可以更紧。
引理 2:对于任意一个结点 \(u\) 到根的链,会被划分为 \(k\leq \log n\) 段重链前缀。假设这 \(k\) 段重链的顶端按照从上到下的顺序依次为 \(z_1, z_2, \cdots, z_k\),那么一定有 \(size(z_i)\leq n/2^{i-1}\)。
由于 \(size(z_i)\leq size(z_{i-1})/2\),这就是显然的。
考虑将合并的过程建树,对这个合并树做重链剖分,按照优先走重儿子的方法跑线段树合并。然后对线段树合并写垃圾回收。在节点 \(u\) 时,会有 \(k\) 棵线段树,第 \(i\) 棵线段树挂在 \(z_i\) 上,其操作次数为 \(O(n/2^{i-1})\),那么总节点个数为
记(先假装下式是收敛的)
那么
所以
所以 \(S=1\),那么原式就是 \(O(n)\) 的。
*线段树合并维护树链的并
例题(CF1276F Asterisk Substrings - 洛谷 的线段树合并部分)
*李超线段树合并
例题(CF932F Escape Through Leaf - 洛谷)
李超线段树由于其惊人的结构,它的合并是特殊的。
复杂度:观察每条直线的深度。每次 merge 都需要一次 insert,且 merge 不会改变每条直线的深度,只有 insert 会改变,所以 merge 的复杂度不超过 insert。每次 insert 递归的时候,就会有一条直线的深度 \(+1\) 或者消失,而全局共有 \(O(n)\) 条直线,深度的总和不超过 \(O(n\log n)\),所以复杂度不超过 \(O(n\log n)\)。
*陈亮舟线段树合并
陈亮舟线段树(“楼房重建”线段树):P4198 楼房重建 - 洛谷
应用:【PR #15】二叉搜索树 - Problem - Public Judge
*线段树合并优化 dp
例题(P6773 [NOI2020] 命运 - 洛谷)
给定一棵树 \(T = (V, E)\) 和点对集合 \(\mathcal Q \subseteq V \times V\) ,满足对于所有 \((u, v) \in \mathcal Q\),都有 \(u \neq v\),并且 \(u\) 是 \(v\) 在树 \(T\) 上的祖先。其中 \(V\) 和 \(E\) 分别代表树 \(T\) 的结点集和边集。求有多少个不同的函数 \(f\) : \(E \to \{0, 1\}\)(将每条边 \(e \in E\) 的 \(f(e)\) 值置为 \(0\) 或 \(1\)),满足对于任何 \((u, v) \in \mathcal Q\),都存在 \(u\) 到 \(v\) 路径上的一条边 \(e\) 使得 \(f(e) = 1\)。由于答案可能非常大,你只需要输出结果对 \(998,244,353\)(一个素数)取模的结果。
全部数据满足:\(n \leq 5 \times 10^5\),\(m \leq 5 \times 10^5\)。输入构成一棵树,并且对于 \(1 \leq i \leq m\),\(u_i\) 始终为 \(v_i\) 的祖先结点。