点分治学习笔记
什么是点分治
点分治是一种能够比较方便的处理树上的路径问题的工具,以下有一个简单的例子:
给出一棵n个节点的树和一个整数k,问树上距离是k的倍数的点对有多少。
一个显然的做法是暴力选择两个点,然后 $\text{DFS}$ 暴力找路径,复杂度 $O(n^3)$ 。
$n>1000$ ,哦豁完蛋。
记录每个到根的距离然后每次枚举点对的时候倍增/树剖求 $\text{LCA}$ ?
$n>10^4$ 后这种复杂度为 $O(n^2logn)$ 的做法也稳定TLE了,所以呢?
一种做法是找到一个根,然后遍历这个子树中的每个点,依次处理每个点的子树答案。
在子树里可以直接把所有点到根的路径长度对 $k$ 取模之后排序,然后就可以用乘法原理快速求出答案(
期望复杂度 $O(nklogn)$ (大概吧
为了实现上述做法就回到了这篇文章的话题:点分治。
点分治原理
接着上面的说法,我们可以想到对于一条路径,有且仅有以下两种情况:
- 在子树中,不经过根节点
- 从一棵子树跨过根节点到另外一棵子树
显然我们会发现:随着往下递归的进行,所有的情况1最后都会变成情况2,所以我们只用考虑这一种情况便好。然后我们又可以发现:在子树中只要记录每一个点到当前子树根节点的距离就可以算出任意两点间的路径长。
这就是分治的基本原理。
选择重心
说到底点分治还是分治,也就是简单的把问题分成几个子问题然后合并,但是怎么分与怎么合并,这些都是问题。
显然我们可以想到的是选择子树根节点的时候不能随便选,因为分治问题一般都是递归处理,所以说根的选择会涉及到递归深度的问题,于是就提到点分治中的一个概念:重心。我们把一棵无根树中那个作为根后可以使得最大的子树最小的点称为这棵树的重心。
求重心不是一个麻烦问题,树形 $\text{DP}$ 就能轻松解决,复杂度 $O(n)$ 。
以下是几个会用到的变量:
- $siz[i]$ 以 $i$ 为根节点的子树的节点数
- $sum$ 总结点数
- $f[i]$ 以 $i$ 为根节点的所有子树中最大子树的 $siz$
然后由于并不复杂,代码很好读懂,我也就不解释了(懒.jpg
1 | il void Get_Root(int x, int pre) |
点分治的实现
有了以上的东西之后,点分治就很好实现了
首先仍然是几个(其实只有一个)变量:
- $vis[i]$ 用与记录节点 $i$ 是否被分治过
然后由于并不难理解,也直接放代码:
1 | il void Solve(int x) |
需要注意的是为了是每一次递归都是最优的,所以递归到下一层前需要先求出子树的重心。
另外,这里的 $calc$ 函数是一个统计答案的函数,因题而异。
而且由于题目的不同 $Solve$ 函数也会发生变化,所以这里记板子并没有什么作用。
关于统计答案发生错误的那档事
如果你按照上面的 $Solve$ 函数去打的话,你会发现答案是错的。为什么呢?其实我们仔细想一想就会发现问题了:
在上图中我们分治到节点1的子树后我们会认为节点5和节点6的距离是4+4=8
,但这显然是错误的,所以在往下递归之前要把这些多算的东西减掉。
然后就变成了这样:
1 | il void Solve(int x) |
所以开头的那道例题
我就不讲了(反正都是我瞎yy的,我才懒得写呢
一些例题
1.【国家集训队】聪聪可可
题意
给出一棵树,问两个点之间的距离能被3整除的概率是多少
思路
显然是一道裸题,用t[i]
表示子树中到根的距离模3为i
的点对数量,显然在一棵子树中的答案就是
所以就只用DFS
一遍就可以解决了
代码
1 |
|
2.[POJ1741]Tree
题意
给出一棵树,问有多少点对的距离小于k
思路
在每一层分治的时候,记录一下每个点到当前重心的距离,然后双指针扫一下就完了
代码
1 |
|
3.[Luogu3806]【模板】点分治1
题意
给出一棵树,问是否有距离为k
的点对
题解
每一层分治的时候处理出子树中每个点到当前中心的距离,然后暴力枚举子树中的所有点对统计即可,最后查询的时候看距离为k
的点对数量是否大于0即可
代码
1 |
|
4.[FJOI2014]最短路径树问题
题意
给出一张图,问在它字典序最小的最短路径树上经过k
个节点的最长路径的长度与条数
思路
SB拼板题,套一个Dijkstra
板子和一个点分治的板子就完了
点分治的部分就是递归下去后记录一下当前深度的最长路径与条数再转移一下就行了
所以这其实是一道长链剖分习题.jpg
代码
1 |
|
咕咕咕
如果我做到什么有意思的点分题再更新(