点分治 fu 言 lan 语

发布时间 2023-07-14 00:10:59作者: ckain

906eda7b-809c-4baa-a171-f930a8847c7a

考虑一类恶心的树上问题.它需要考虑所有的树上路径.

点分治专用于解决这样的问题.


枚举路径的中转点(给路径分类).

首先找到树上任意一个点 \(u\).那么路径可以分为两类:经过点 \(u\),不经过点 \(u\).将 \(u\) 作为根提起.任意两个 不同子树 中的节点到 \(u\) 的路径拼接在一起均可形成不同的经过 \(u\) 的路径.同时,不需要拼接时,任意一个子树中的点本身也是经过 \(u\) 的路径.

剩下的路径都是不经过 \(u\) 的.考虑删去 \(u\) 点,剩下的部分是一些独立的小树.递归の实现上面的流程即可.

为了 优化时间,每次枚举寻找当前树的重心.这样时间复杂度就会得到奇妙的提升,来到了匪夷所思的 \(O(n \log n)\)

值得注意的是,\(O(n \log n)\) 要求每次分治时仅遍历当前树:当前树上每个节点仅进行 \(O(1)\) 次运算.这说明如果要求有效率地解决分治问题,不能暴力枚举不同子树的点对(稍微思考也可以发现:如果暴力枚举点对,会遍历到每个路径一次.时间复杂度显然 \(O(n^2)\)).

故点分治算法的精髓在于给路径分类后再用乘法原理压缩暴力枚举路径的时间.


很多时候人们喜欢直观的事情.但是有时看似直观的事情实现起来可能更加复杂.如果直观的做法实现起来同其看上去一样简洁,就不会有更复杂的做法出现了.

存在即合理

关于点分治枚举路径的方法大致分为两种流派.

第一种是每搜索一棵子树后,考虑前面的子树对于它的贡献.然后在把它并到前面去.看似很美好,其实存在许多问题:比如如果要在端点上统计贡献,按子树顺序扫描只能记到前面子树的端点对自己的贡献.如何解决?倒着枚举一遍子树.然后代码会爆炸性复杂.

第二种是容斥.假如当前树的根是 \(Rt\).每个点到 \(Rt\) 的路径都是可拼接的一段.我们用点对\((u,v)\)表示一条路径 \(u \rightarrow Rt \rightarrow v\),注意是有向的.考虑什么样的拼接是有意义的,什么样的拼接是无意义的.发现无意义的拼接只有一种:

\[(u,v)(u,v\ in\ the\ same\ subtree) \]

我们先枚举所有的拼接(即当前树中的任两个组合):

\[\begin{aligned} &(1). (u,v)(u,v\ in\ the\ same\ subtree)*\\ &(2). (u,v)(u,v\ in\ the\ different\ subtree)\\ &(3). (u,Rt)\\ &(4). (Rt,Rt) \end{aligned} \]

去除 \((1)\) 即可.
具体来说:枚举子树的根 \(subRt\),每个当前子树中的点 \(u\) 代表路径为 \(u \rightarrow subRt \rightarrow Rt\).则任意当前子树中的点对 \((u,v)\) 代表的路径拼接为 \(u \rightarrow subRt \rightarrow Rt \rightarrow subRt \rightarrow v\).这都是该去掉的.并且发现每个该去掉的点对都唯一存在于某一个子树中.删掉所有这样的点对即可(即子树中的任两个组合).

分析这种算法的效果:对于每个点,刚好不重不漏地枚举到了其到每个点的路径.对于每条路径,若其为单独的一个点,其被枚举到了一次,即某个 \((Rt,Rt)\);若长度 \(>1\),则其被枚举到了两次:\(u \rightarrow v,v\rightarrow u\)

综上所述:若纯粹是路径贡献求和问题(只需要计算某种特殊路径的贡献,可差分),不在端点统计,两种方法的可写性基本一致.若需要统计每个端点的答案,则容斥有其优势.当然,如果是限制性条件的最优化问题,容斥就做不了了.