前言
点分治,是一种处理树上路径问题的工具,举个例子:
给定一棵树和一个整数 \(k\),求树上边数等于 \(k\) 的路径有多少条。
做法很简单,枚举不同的两个点,然后 dfs 算出它们间的距离,统计一下就行了。大概是 \(O(n^3)\) 的复杂度。
布星啊 \(n\) 大一点就搞不了。
那找个根,求出每个点到根的距离,然后枚举两个点,求 lca,简单加减一下就行了。
大概是 \(O(n^2\log n)\) 的复杂度?
还是布星啊 \(n\) 还要大,几万的数据就搞不了了怎么这么多事……
考虑一下形成路径的情况,假设一条满足条件的路径经过点 \(x\),那么这条路径在 \(x\) 的一个子树里(以 \(x\) 为端点)或者在 \(x\) 的两个不同的子树里,如图:
一个好的想法是找到一个根,然后 \(dfs\) 遍历子树中的每个点,依次处理每个点的子树答案。
原理
如图,假设我们选出一个根 Root,那么答案路径肯定是要么被一个子树所包含,要么就是跨过 Root,在黑子树中选择一部分路径,在红子树中选择一部分路径,然后从 Root 处拼起来形成一条答案路径。
仔细想一下,发现情况 \(1\)(被一个子树包含)中,答案路径上的一点变为根 Root,就成了情况 \(2\)(在两棵子树中)
如图,Root 为根的子树中存在答案(蓝色实边路径),可以看成以 Root 为根的两棵子树存在答案,所以只用处理情况2就行了,可以用分治的方法,这是点分治的基本原理。
先从找好一个根开始。
过程
选根
首先根不能随便选,选根不同会影下面遍历的效率的,如图:
显然选 \(y\) 为根比选 \(x\) 为根不优,选 \(x\) 最多递归 \(2\) 层,选 \(y\) 最多递归 \(4\) 层。
可以发现找树的重心(重心所有的子树的大小都不超过整个树大小的一半)是最优的。
我们可以根据每个点子树大小确定根,当根的最大的子树最小时肯定是重心。
一个简单的树形 DP 就能搞定。
void getroot(int u,int fa) {
siz[u]=1,f[u]=0;
for(int i=head[u]; i; i=e[i].nxt) {
int v=e[i].to;
if(v==fa||vis[v]) {
continue;
}
getroot(v,u),siz[u]+=siz[v],f[u]=std::max(siz[v],f[u]);
}
f[u]=std::max(sum-siz[u],f[u]);
if(f[u]<f[r]) {
r=u;
}
}
因为之后的分治过程还需要对子树单独找重心,所以代码中有 \(vis\),但是开始对整棵树无影响。
求距离
找到根了,现在我们可以 dfs 一遍重心的子树,求出重心到子树各个点的距离。
然后可以枚举子树里的两个点,如果两个点到重心的距离和为 \(k\)(题目要找距离为 \(k\) 的点对),那么答案 \(+1\)。
这是第二种情况,第一种情况就让距离根为 \(k\) 的点跟重心配对就行了,因为重心到重心的距离为 \(0\)。
统计答案
肯定不能直接枚举啊……\(n^2\) 的复杂度啊!
考虑枚举一个点,另一个点可以通过二分来求解,sort
一下让距离有序,这样要找距离为 \(k\)——枚举点的距离的点的个数,因为相同距离的点现在是连续的,所以可以二分出左右边界\(l,r\),\(ans+=r-l+1\)。
也可以通过移动两个指针来实现只要不是枚举两个点就行了。
这样我们就快乐的A掉了这道题……了吗?
求一遍发现答案不对诶!似乎多了几种情况?如图:
假设 \(k=4\),图中 \(A\) 到 Root 的距离为 \(2\),\(B\) 到 Root 的距离为 \(2\),合起来是 \(4\),这时候答案 \(+1\),但是显然这两个点最短路径不是 \(4\)!这是因为它们在同一子树中,到重心的路径有重叠部分。
这时要怎么处理呢?
- 可以求距离的时候把点染色,不同子树不同颜色,那么求答案的时候就得枚举每个符合答案的每个点看是否不在一个子树里。
- 可以求当前点儿子的答案,统计儿子答案时各个点的距离加上儿子到根的距离,即把符合在一个子树条件的情况统计出来,最后这个点的答案减去儿子答案就行了。
图中求 Root 儿子 son 的答案,因为加上儿子到重心的距离,所以 \(A\) 的距离还是 \(2\),\(B\) 的距离还是 \(2\),这样就把不符合条件的答案去掉了。
int cal(int u,int x) {
dis[u]=x,px[0]=0,getdis(u,u),std::sort(px+1,px+px[0]+1);
int temp=0;
for(int i=1,j=px[0]; i<j; ) {
if(px[i]+px[j]<=k) {
temp+=j-i++;
} else {
j--;
}
}
return temp;
}
void solve(int u) {
vis[u]=1,ans+=cal(u,0);
for(int i=head[u]; i; i=e[i].nxt) {
int v=e[i].to,w=e[i].w;
if(vis[v]) {
continue;
}
ans-=cal(v,w),r=0,sum=siz[v],getroot(v,u),solve(r);
}
}
这样答案就对了。
复杂度
每次处理找树的重心,保证递归层数不超过 \(\log n\),dfs 求距离复杂度是 \(O(n)\),这里处理答案是 \(\log n\),所以这个题总复杂度是 \(O(n\log^2 n)\)。
注意:因为有的题可以用桶排序,所以复杂度可以降到 \(O(\log n)\)。当然有的题桶开不下必须 sort
。