点分治
点分治
点分治常用于解决树上路径统计问题。它的基本思路是:选定某个点 ,将路径划分为经过 和不经过 两类。对于前一种,我们直接处理。对于后一种,我们递归,在子树中处理。
首先要解决的是递归的层数,考虑一条链,如果从链的一端开始,每次向子结点递归,那么递归层数是 的。解决方法是每次选取重心进行递归:即先处理整棵树的重心,然后递归处理每棵子树的重心。这样每次子树大小至少减少一半,所以递归层数是 级别的。
然后我们要解决的问题就是:选定某个根,统计经过它的路径数量。这个因题而异,没有统一的方法。一种比较常见的思路是逐棵子树统计,并合并答案。我们以一道例题来具体说明。
例 P3806 【模板】点分治1
题目链接:https://www.luogu.com.cn/problem/P3806
题意:
给定一棵有 个点的树。 次询问,每次询问树上是否存在距离为 的点对。。
题解:
我们考虑对于给定的根,如何解决。显然,我们可以遍历每棵子树,求出当前子树内的点到根的距离,对于每个距离 ,枚举询问,判断前面的子树中是否有距离为 的点,若存在,则这两个点距离为 且经过根,符合。最后把所有点的距离保存下来,处理下一棵子树。
这样单个点的时间复杂度为 ,总的时间复杂度为 。
然后看一下具体实现:
1 |
|
这里 sum
表示当前处理的子树大小,getsiz
函数用于求出当前子树的重心,做两遍的目的是求出以重心为根时每棵子树的大小,这样分治处理的时候可以更新sum
。vis
数组的作用是标记已经处理过的点。
1 |
|
getdist
函数的作用是计算每棵子树内的点到根结点的距离,并保存在vec
中。在点分治的过程中,我们标记已经处理过的点,遍历每棵子树,得到vec
。然后根据exist
(记录前面子树中已经出现距离)更新答案。最后将vec
中的距离写入exist
中,直到所有子树都遍历完成。
当该点处理完毕后,exist
数组需要清空,所以我们可以用一个队列q1
来记录处理该点时,一共有哪些距离被加入。(直接暴力清空会TLE)
最后,我们递归处理每一棵子树。
附上完整代码:
1 |
|
以下是一些练习题:
P4178 Tree
题意:
给出一棵 个结点的树,求距离小于等于 的点对数量。。
题解:
本题和上一题的区别在于由等于 变成了小于等于 。可以想到一个很直接的处理方式:用权值线段树来记录出现过的点。枚举当前子树,对于该子树内每个点与根的距离 ,它对答案的贡献为距离小于等于 的点个数和。
代码:
1 |
|
P2634 [国家集训队] 聪聪可可
题意:
给出一个 个结点的树,任意选取树上两点,求两点间距离能被 整除的概率。。
题解:
只需要维护模 余 的路径数量即可。
注意:由于子树依次加入,所以模板中求出的点对是无序的,而这里是有序的。同时,还要加上两个点重合的情况。
代码:
1 |
|
CF1101D GCD Counting
题意:
给出一棵有 个点的树,每个点有点权 。求一条最长的路径长度,使得路径上所有点点权的 大于 。。
题解:
对于每个点,我们枚举它的质因子 ,分别处理出点权 等于 的最长路径长度,这是容易维护的。而 范围内的整数最多只有 个质因子,时间复杂度为 。
代码:
1 |
|
*P2664 树上游戏
题意:
给出一棵有 个点的树,树的每个节点有个颜色。定义 为 到 的颜色数量。以及。求出所有的 。。
题解:
(感觉这题理解得还不是很透彻)
考虑固定的分治中心 。记 表示以 为一个端点且含有颜色 的路径数量,则 。 很容易处理,进行dfs,每遇到一个新的颜色就加上它的子树大小。并且它对子树信息的合并很有帮助。
对于固定的分治中心 ,我们只需要关心经过它的路径。可以分为两类,一类是以分治中心为端点的路径,一类是端点位于分治中心的两棵子树内的路径。
对于第一类,我们在遍历子树的时候就可以顺便统计。对于第二类,我们再分为两部分。设当前处理的点为 ,对于 路径上出现的颜色 ,它对 的贡献为其它子树的大小,对于没有出现的颜色 ,它对 的贡献为其它子树的 之和。
在实际实现的时候,我们可以考虑正反序各遍历一遍子树,计算每个点与它前面子树所有点的路径和每个点与它后面子树所有点的路径。同时,对于路径上出现的每一种颜色 ,还需要扣除掉其他子树内的 ,因为它不能重复计算。
代码:
1 |
|