写的有点乱,意识流 blog /yun
感谢 @Mr_Spade 对我的帮助。
从 Dsu on tree 到点分治
Dsu on Tree 的主要思想是轻重链剖分,使每个点作为根时,都继承重儿子的信息并快速加入根的影响,并对每个轻儿子暴力向重儿子合并。由于每个点被合并 \(O(\log n)\) 次,复杂度是正确的。
这种问题也可以用点分治实现。对于无根树上的问题,对点分得到的若*分分别遍历,然后用和上面同样的数据结构维护查询和插入操作。
而对于有根树中“对每个子树求解”的问题则相对复杂。考虑有根点分治,记分治中心为 \(x\),\(x\) 下方的子树为 \(x_1, x_2,\cdots, x_k\),\(x\) 上方的祖先为 \(y_1,y_2,\cdots, y_p\),整个连通块的根为 \(r\)(不包含在 \(y\) 中)。考虑先计算子树 \(x\) 下方的答案,递归 \(x_{1\sim k}\)。合并和上面是一样的。然后对于每个 \(y_i\) 和 \(r\) 都分别递归,然后将其依次并入 \(x\) 子树的信息中。途中可以直接完成 \(x, y_{1\sim p}\) 答案的计算——仔细观察点分治的递归方式可以发现除了一些与 \(r\) 之间相连的部分不被当前连通块包含,其他都是完整的,可以准确得到答案。那么对于 \(r\) 我们保留合并得到的信息,接应之后可能的合并。
可以看到基本上 Dsu on tree 可以做的点分治也可以做,但实际上本质是差不多的。考虑你想要把 \(n\) 个一次多项式乘起来,你可以套用哈夫曼树的方法,维护一个优先队列,每次取两个小的相乘然后放回堆中;或者使用分治,先将 \(f_l\times f_{l+1}\times \cdots \times f_{mid}\) 以及 \(f_{mid+1}\times f_{mid+2}\times \cdots \times f_r\) 算好,再计算 \([l,r]\) 全部相乘的结果。这分别对应着 Dsu on tree 和点分治,两者都是将 \(n\) 个点合并,只是保证复杂度的方式不同。
一般而言做有根这种题都优选 Dsu on tree,因为效果几乎一样并且常数和代码都比较优秀。上面这段文字只是说明了:
Dsu on tree 能解决的问题为点分治的真子集
听起来是暴论但实际上几乎正确。考虑 Dsu on tree 的操作条件是:可以快速对重儿子加上根的影响;并且支持将轻儿子的内容高效动态插入。
然而一旦满足这些条件,点分治也可以操作。但你发现在这方面的问题点分治的做法都比较繁琐。这是因为点分治的擅长之处完全不在动态插入类问题。
点分治与静态计算
考虑这样一个问题:
给定两棵树 \(S,T\),带边权且 不一定非负。求 \(\max\limits_{u,v}\ \textit{dist}_S(u,v)+\textit{dist}_T(u,v)\)。
由于边权非负,合并点集并由原来两对最远点得到新的最远点的结论已不适用了。下面需要用到一个合并优化的技巧:
合并优化:对于当前分治中心连接的若*分 \(x_1, x_2, \cdots, x_k\),每次选取大小最小的合并并放回作为一个新的部分。这样可以证明效率能达到边分治一样的复杂度,具体证法可见 zzy 论文(?)。这个 trick 的功效在于,可以想边分治一样只用考虑两个不同的部分而不是多个,可以为解题扫清一些障碍。
对于两个在树 \(S\) 上的点集 \(V_1,V_2\),我们建出 \(V_1\cup V_2\) 在 \(T\) 上的虚树。首先求出 \(S\) 中每个点到分治中心的距离作为在 \(T\) 虚树 \(T'\) 的点权。那么只要得到 \(T'\) 上距离加上两边点权的最大值即可。具体实现类似于树型 dp。可以发现这实际上实现了一个静态问题的求解过程:建虚树然后 dp。
那么如果强行转化为动态又将如何?设我们维护出 \(V\subseteq S\),然后一个新的点 \(x\) 需要被计算贡献。那么唯一的办法是求出 \(T\) 上 \(V\) 中的点的点权加上与 \(x\) 距离的最大值,这本身就是极其复杂的问题。因此 Dsu on tree 直接怎么做是不太行的。
可以发现,点分治擅长统计可以静态计算的信息。动态维护往往会比前者更加困难,或者是被转化为一个“更强的问题”。那也不奇怪为什么 Dsu on tree 的适用范围更小了。
快速添加根的影响?
这是经常忽略的一个要点。考虑这样一个题:
\(n\) 个点的树,每个点 \(i\) 有一次多项式 \(x-a_i\)。求每个点到根结点 \(1\) 路径上多项式的乘积,输出它们求和后的结果。
这题有一个基于长链剖分链分治的做法,这里略去。
暴力做法是对子结点求和,并乘上自己,复杂度平方。考虑有根点分治(一下记号和上文一致):对于分治中心 \(x\),我们先递归计算 \(x_1, x_2, \cdots, x_k\),对它们的多项式求和记作 \(g\),这里复杂度不超过 \(x\) 子树大小。然后递归 \(x\) 父亲方向的连通块,那么这时除了子树 \(x\) 都完成对 \(r\) 贡献的统计了。
唯一的问题是 \(x\to r\) 的多项式 \(f\) 怎么算。比较无脑的有分治 FFT,这样总复杂度 \(O(n\log ^3 n)\),可以利用之前递归子问题结果的技巧将其去掉一个 \(\log\),这个暂且不谈。
尝试使用 Dsu on tree?对于重儿子求出答案,乘上根直接作为根的答案,然后对所有轻儿子合并?不幸的是“乘上根”这个操作复杂度就和重子树相关了——多项式乘法没法像数据结构那样快速添加影响。实际上稍微冷静一下发现这玩意就是暴力。
更多的时候,这一点可能表现为”难以全局打 tag“之类的情况。
本质原因……
具体的,dsu on tree 的短板归纳为两点:
- 在静态计算简单但维护动态添加困难的时候,无法快速合并轻子树;
- 无法高效加入根的影响;
可以发现上面两点说明了一件事:dsu on tree 高度依赖动态添加的高效方法。
为什么会这样?注意到点分治之所以可以随意遍历整个连通块,是因为重心分解的分治结构做了保障。而 Dsu on tree 却必须靠规避重子树来保证复杂度。
例子
给定一颗树,带边权,求路径上边权平均值 \(\in [L,R]\) 的路径数。
首先记 \((c,s)\) 为一条边数为 \(c\) 总权值为 \(s\) 的路径。那么对于两条路径 \((c_1,s_1),(c_2,s_2)\),由合法条件可以推出两个不等式:
\[s_1-c_1R\le -(s_2-c_2R) \\ s_1-c_1L\ge -(s_2-c_2L) \\ \]这个式子一边都只与一条路径有关,抽象成两维坐标就是数点问题了。考虑点分治做法,如果是采用合并优化并静态统计的方法其实很容易,只是简单的扫描线和数据结构。这样总复杂度为 \(O(n\log^2 n)\)。
而另一种动态添加的思路就比较困难——天然的强制在线使得问题棘手了很多,我们似乎避不开高级数据结构,甚至可能还有更劣的复杂度。