写的有点乱,意识流 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_{1sim k})。合并和上面是一样的。然后对于每个 (y_i) 和 (r) 都分别递归,然后将其依次并入 (x) 子树的信息中。途中可以直接完成 (x, y_{1sim p}) 答案的计算——仔细观察点分治的递归方式可以发现除了一些与 (r) 之间相连的部分不被当前连通块包含,其他都是完整的,可以准确得到答案。那么对于 (r) 我们保留合并得到的信息,接应之后可能的合并。
可以看到基本上 Dsu on tree 可以做的点分治也可以做,但实际上本质是差不多的。考虑你想要把 (n) 个一次多项式乘起来,你可以套用哈夫曼树的方法,维护一个优先队列,每次取两个小的相乘然后放回堆中;或者使用分治,先将 (f_l imes f_{l+1} imes cdots imes f_{mid}) 以及 (f_{mid+1} imes f_{mid+2} imes cdots imes f_r) 算好,再计算 ([l,r]) 全部相乘的结果。这分别对应着 Dsu on tree 和点分治,两者都是将 (n) 个点合并,只是保证复杂度的方式不同。
一般而言做有根这种题都优选 Dsu on tree,因为效果几乎一样并且常数和代码都比较优秀。上面这段文字只是说明了:
Dsu on tree 能解决的问题为点分治的真子集
听起来是暴论但实际上几乎正确。考虑 Dsu on tree 的操作条件是:可以快速对重儿子加上根的影响;并且支持将轻儿子的内容高效动态插入。
然而一旦满足这些条件,点分治也可以操作。但你发现在这方面的问题点分治的做法都比较繁琐。这是因为点分治的擅长之处完全不在动态插入类问题。
点分治与静态计算
考虑这样一个问题:
给定两棵树 (S,T),带边权且 不一定非负。求 (maxlimits_{u,v} extit{dist}_S(u,v)+ extit{dist}_T(u,v))。
由于边权非负,合并点集并由原来两对最远点得到新的最远点的结论已不适用了。下面需要用到一个合并优化的技巧:
合并优化:对于当前分治中心连接的若干部分 (x_1, x_2, cdots, x_k),每次选取大小最小的合并并放回作为一个新的部分。这样可以证明效率能达到边分治一样的复杂度,具体证法可见 zzy 论文(?)。这个 trick 的功效在于,可以想边分治一样只用考虑两个不同的部分而不是多个,可以为解题扫清一些障碍。
对于两个在树 (S) 上的点集 (V_1,V_2),我们建出 (V_1cup V_2) 在 (T) 上的虚树。首先求出 (S) 中每个点到分治中心的距离作为在 (T) 虚树 (T') 的点权。那么只要得到 (T') 上距离加上两边点权的最大值即可。具体实现类似于树型 dp。可以发现这实际上实现了一个静态问题的求解过程:建虚树然后 dp。
那么如果强行转化为动态又将如何?设我们维护出 (Vsubseteq 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 o r) 的多项式 (f) 怎么算。比较无脑的有分治 FFT,这样总复杂度 (O(nlog ^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)),由合法条件可以推出两个不等式:
这个式子一边都只与一条路径有关,抽象成两维坐标就是数点问题了。考虑点分治做法,如果是采用合并优化并静态统计的方法其实很容易,只是简单的扫描线和数据结构。这样总复杂度为 (O(nlog^2 n))。
而另一种动态添加的思路就比较困难——天然的强制在线使得问题棘手了很多,我们似乎避不开高级数据结构,甚至可能还有更劣的复杂度。