从树分块到树上莫队
本文主要讲解树分块。树上莫队部分则主要将基于树分块的树上莫队,实用性较低,建议选择性阅读。
树分块
假如现在有一个线段树等朴素分治结构不支持维护的信息,要你支持在树链上查询,例如在线支持树链加、树链查
记树的点数为
-
假如树退化成一条链,也就是这个东西被放在序列上,这是一个经典题。防止有人不知道复杂度怎么来的,这里说一遍。
考虑对序列以
B 为块长分块,在每个块内维护块排序后的结果。每次被散块加的块直接重构掉整个块,可以利用原有信息归并\mathcal{O}(B) 计算;整块加打标记即可,单次操作\mathcal{O}\left (\dfrac{n}{B}\right) ;散块查询直接暴力统计个数,单次操作\mathcal{O} (B) ;整块查询则考虑在整块维护的有序序列上二分答案即可,单次操作可以做到\mathcal{O}\left (\dfrac{n \log n}{B}\right) 。取
B=\mathcal{O}(\sqrt{n\log n}) ,此时时间复杂度为\mathcal{O}(m\sqrt{n\log n}) 。 -
对于一般情况的树,考虑重链剖分,对于 dfn 序的序列套用上述分块,单次操作每次跳重链都是一个区间操作,相当于将一个操作拆成了
\mathcal{O}(\log n) 次序列操作。直接与 1. 的复杂度相乘得到该算法时间复杂度为\mathcal{O}(m\sqrt{n}\log^{1.5}n) 。但事实上,对于一个操作而言,每次跳过的重链是互不相交的,即它们在 dfn 序列上也互不相交。所以一个序列上的块在单次树上操作最多被整块操作一次,故整块操作还可以去一个
\log 。即单次树上修改复杂度为
\mathcal{O}\left (B \log n + \dfrac{n}{B}\right ) ,单次树上查询复杂度为\mathcal{O}\left ((B + \dfrac{n}{B})\log n\right ) 。查询复杂度严格大于修改了,取
B=\mathcal{O}(\sqrt{n}) 即可,此时时间复杂度为\mathcal{O}(m\sqrt{n}\log n) 。 -
还是考虑重链剖分,这次我们直接对每条重链单独分块。即对于长度为
k 的一条重链,我们以B=\mathcal{O}(\sqrt{k\log k}) 为块长对其分块。然后我们与 2. 相同地进行操作。由于跳过一条轻边子树大小至多是原先的
\dfrac{1}{2} ,而重链长度一定不超过重链顶端的子树大小,所以在一条树链上的第i 条重链长度是\mathcal{O}\left ( \dfrac{n}{2^i}\right) 的。对一次操作中每条重链带入 1. 的复杂度,复杂度为
\displaystyle \sum _{i}\sqrt{\dfrac{n\log n}{2^i}}=\mathcal{O}(\sqrt{n \log n}) 。即总时间复杂度为
\mathcal{O}(m\sqrt{n \log n}) 。这和序列上做到了相同的复杂度,可以说明这一定已经最优了。
但是聪明的你发现,上面所有的针对一般树的做法都需要将若干链上的答案合并起来成为询问的答案。假如答案是不可合并的信息,那上述的东西全都是扯淡了。比如在线树链数颜色(P6177)。
还有一个缺点,我们在序列上常常使用如
上面的东西本质上都是换着花样的序列分块,考虑如何真正的对「树」分块。
有两种方法:
-
随机撒点树分块:
设定阈值
B ,在树上随机撒\dfrac{n}{B} 个点,然后对这些点建立虚树,我们称虚树上的点为关键点。关键点的数量是\mathcal{O}\left (\dfrac{n}{B}\right ) 个(严格\le 2 \cdot \dfrac{n}{B} 个)。对于任意一条树上的长度为
L 的链。期望下这条链上存在\dfrac{LB}{n} 个关键点且均匀分布。故一条链被分为\Theta\left (\dfrac{L}{B}\right ) 个长度为\Theta(B) 的关键点间的路径。这也说明了两个相邻关键点的距离为\Theta(B) 。 -
top cluster 树分块:
设定阈值
B ,对树进行 DFS,维护一个存贮边的栈,每条边在向上回溯时加入,则此时对应子树中所有的边都被加入过。当前点
u 要结束 DFS 的时候,如果出现以下三种情况,我们将栈清空并将点u 记为关键点。- 点
u 是树的根结点。(只是为了处理方便) - 点
u 的至少有2 个儿子v ,使得以v 为根的子树中存在关键点。(使关键点形成树形结构,我们称其为收缩树) - 栈中有至少
B 条边。
我们发现其实我们只关心栈中边的数量,所以我们只需在点
u 结束 DFS 后,记录此时栈中剩余的边数h_u 。只需在触发上面三种情况的时候让
h_u \gets 1 ,否则h_u \gets 1+\sum \limits _{i \in \mathrm{son}(u)}h_i 。显然第三种情况最多出现
\dfrac{n}{B} 次,而收缩树的叶子结点一定由这种情况提供,所以收缩树的叶子数\le \dfrac{n}{B} ,故弹栈次数\le 2 \cdot \dfrac{n}{B} 。即关键点数为\mathcal{O}\left(\dfrac{n}{B}\right) 级别。我们将块改称为簇。我们将两个相邻关键点中间的部分划分出一个簇。这两个关键点构成祖先关系,我们分别叫它们上界点和下界点。任意两个簇有至多一个点相交。
例如上图中
0,4,5,8,10 是界点。有时我们需要将点
u 不同儿子划分到不同的簇中,考虑将所有儿子依次列出,记其有k 个儿子,a_i 为第i 个儿子的h ,b_i 表示以第i 个儿子为根的子树中是否有关键点。因为情况三,显然有
a_i \le B ,否则其一定会触发清空。我们将
[1,k] 划分为若干段,对于其中的一段[l,r] 需要满足\sum \limits_{i=l}^{r}a_i \ge B 且\sum \limits_{i=l}^{r-1}a_i < B 且\sum \limits_{i=l}^{r}b_i \le 1 ,最后还有至多一段\sum \limits_{i=l}^{r}a_i < B 的特殊情况加入。比较容易证明簇的数量是
\le 6 \cdot \dfrac{n}{B} 。 - 点
两种方法的对比:
| 关键点(界点)的数量 | 块(簇)直径长度 | 块(簇)结点数 | 块(簇)的个数 | |
|---|---|---|---|---|
| 随机撒点树分块 | ||||
| top cluster 树分块 |
注:随机撒点树分块如果将每个子树中没有关键点的结点归于其第一个关键点祖先,则块(簇)的个数
例题
给定一个
n 个节点的树,点有权值。有m 次询问,每次给出u,v ,查询u 到v 的路径上有多少个不同的整数。强制在线。
离散化后值域变为
对序列上分块数颜色类比,我们预处理任意两个关键点之间的颜色数。单次询问链
每暴力加入一个新的散块元素,我们都需要知道这个元素是否已经在之前被加入过,也就是考虑其出现次数。分为在之前加入散块与整块两种,前者可以维护一个桶,后者考虑维护树上前缀和。
也就是
因为
此时
维护两个关键点间的答案,可以从每个关键点向外 DFS,容易做到
取
例题
给定一棵
n 个点的树,点有点权a_i ,m 次询问,其中第j 次询问给出t_j 条路径,求这t_j 条路径并集中点权的不同数个数以及\mathrm{mex} 。强制在线。其中
\mathrm{mex}(S) 表示不在集合S 中出现的最小非负整数。
值域这么小相当于直接把 bitset 塞你嘴里了。
显然对于每种元素,我们只关心其是否出现,而不关心其具体的出现次数。所以并集算重也没关系,单独考虑每条路径就行。
对于每个询问,我们希望计算出路径并集中每种元素是否出现的 bitset。对于不同点权计数,就是找 bitset 上
考虑随机撒点后建虚树,维护任意两个关键点间路径元素是否出现的 bitset。
对于每条路径,先将中间关键点的路径点集加入答案,复杂度为
然后就做完了,时间复杂度为
取
例题
给定一棵
n 个节点的无根,有边权的树,m 组询问,每次询问给出l,r ,求\sum \limits _{l \le i < j \le r}d(i,j) 。其中
d(x,y) 表示点x 到点y 的路径长度。
使用莫队二次离线,将问题转换为
经典的,
所以
前两者维护容易。考虑第三个咋做。
同样经典的,对于两条从根
考虑按
这个你肯定要做到
在每个位置
显然,答案可以使用
对于一次修改
所以单次修改可以做到
取
例题
给定一棵
n 个节点的树,m 次操作,每次操作是链加点权或者链求点权和。
容易想到重链剖分,但是树剖要开很多数组,而且点权你不可能不存,为了保存树的形态你还要存父亲,所以即使你尽可能少开数组也绝对跑不了树剖。
考虑一些额外空间较少的结构。
不妨使用树分块。
先考虑如何构建树分块。随机撒点后建立虚树一般需要按 dfn 序排序后查询相邻
这样显然可以找到所有虚树上的点,且时间复杂度为
对每个关键点维护其在虚树上的父亲,空间显然是
对每次操作,为方便将操作差分为四个根链操作,这个过程需要支持查询树上两个点
下界点有点不好找,考虑对于每个关键点预处理下面每个方向遇到的第一个关键点,总量显然是
后面的部分就与上题比较类似了,但是注意不要多开
取
例题
菜菜子给了你一棵
n 个节点的树,有m 个询问,每个询问包含参数p_0,d_0,p_1,d_1 ,求:\sum\limits_{d(p_0,a)\leq d_0}\sum\limits_{d(p_1,b)\leq d_1}d(a,b) 其中
d(x,y) 表示点x 到点y 的路径长度。
????的第七分块。难度评分
记
考虑对树进行树分块。
对于一个块
若
对于任意一个
对于一个询问
现在每次询问我们都要统计
-
若
B_0=B_1 :每次询问这种情况有
\mathcal{O}\left (\dfrac{n}{B}\right) 次。假如
x_0,x_1 中有至少一个点不是界点。这种情况只有\mathcal{O}(m) 次。此时可以考虑暴力求出结果,将d(x,y) 写作\mathrm{dep}(x)+\mathrm{dep}(y)-2\mathrm{dep}(\mathrm{LCA}(x,y)) ,前两个可以直接求出。第三个可以转换为\mathcal{O}(B) 次链加后\mathcal{O}(B) 次链求和。树上差分后建立树上前缀和即可。这部分时间复杂度总共是\mathcal{O}(mB) 。假如
x_0,x_1 都是界点。由于块的直径大小是\mathcal{O}(B) 的。所以y_0,y_1 都分别只有\mathcal{O}(B) 种可能。而x_0,x_1 有4 种组合。故一个块中本质不同的邻域对只有\mathcal{O}(B^2) 种。考虑预处理出所有可能的答案,从小到大枚举
y_0 的大小,每次在前面所有距离<y_0 的基础上扩展出一圈距离=y_0 的点,相当于二维前缀和。这部分复杂度为\mathcal{O}(B^2) 。则这里预处理一共有\mathcal{O}(nB) 的时间复杂度。查询时只需枚举每个块,每个块都是
\mathcal{O}(1) 的,所以这对复杂度的贡献是\mathcal{O}\left( \dfrac{nm}{B} \right) 。 -
若
B_0 \ne B_1 :每次询问这种情况有
\mathcal{O}\left (\dfrac{n^2}{B^2}\right) 次,所以必须同时求解。此时
G_0,G_1 必然互不相交。设G_0 中距离G_1 最近的点是n_0 ,G_1 中距离G_0 最近的点是n_1 。则我们分别在
G_0,G_1 选一个点,这两个点的路径必然经过链(n_0,n_1) 。易知答案为
|G_0| \cdot |G_1| \cdot d(n_0,n_1)+ |G_1| \sum \limits _{i \in G_0}d(i,n_0)+ |G_0| \sum \limits _{i \in G_1}d(i,n_1) 。对于每个
G 而言,只要我们知道|G| 和\sum \limits _{i \in G}d(i,n_o) 即可在收缩树上 dp 求出答案。与 1. 类似,可以直接在
\mathcal{O}(nB) 的时间复杂度内预处理。每次询问有至多\mathcal{O}(1) 个需要暴力\mathcal{O}(B) 处理。
则总时间复杂度为
空间复杂度过高,考虑离线逐块处理,容易将空间降至
我们发现随机撒点树分块可能会划分出
树上莫队
树上莫队有一种人尽皆知的实现方法,大概是对原树 DFS,记录元素的括号序,具体一点就是元素进栈时记录一次,出栈时再记录一次。
记录编号
然后一次查询链
- 若
x,y 呈祖先关系,不妨设x 是上面的点。此时我们查询区间[f_{x,1},f_{y,1}] 。对于在区间中出现了恰好一次的元素就是链上的元素。实现方法可以在第一次碰见某个元素是加入,第二次撤销,也就是状态取反。 - 若
x,y 不呈祖先关系,此时我们查询区间[f_{x,2},f_{y,1}] ,与 1. 使用相同的方法计算即可,但是这样\mathrm{LCA}(x,y) 不会被统计到。查询时加入即可。
其实就是在序列模拟了 dfs 的过程,第一次遇见某个元素时进栈,第二次遇见元素就相当于元素出栈。
可以看作将树压缩成了序列,既然是压缩就肯定失去了什么,不难发现这个莫队不支持回滚。因为它的元素序列中本身就包含删除,如果你不可以支持删除自然就做不了了。
所以说括号序莫队就像是上面用重剖维护树上分块一样拙劣。考虑如何真正的对「树」维护莫队。
维护序列莫队时我们会先对序列分块,每个块大小
类比一下,维护树上莫队,可以考虑树分块。我们对所有询问链
对于位于同一个块的
每次对于两个指针
元素可能需要按顺序加入,所以我们需要按顺序处理出链
以
需要注意不要将
试分析其复杂度,对于一个块,
对于每次询问,
故总复杂度是
回滚的实现与序列上的回滚莫队类似,但是实现细节会非常多。所以目前没有见到树上回滚莫队题目的例子。
参考代码
剪切板链接。
大多数题都使用了
鸣谢
- 感谢我的教练 mpzleon 同意我讲这个,所以我才会写这个东西。
- 感谢 yinianxingkong 在这个帖子里教会了我树上回滚莫队是正确的。
- 感谢在相关题目编写题解,和对树分块和树上莫队进行讲解的作者们。