点分治学习笔记
前言
点分治,适用于树上的路径统计问题,本质上是用分治思想优化的暴力。
其实学会思想就很简单了。
原理
给定一棵树和一个整数 \(k\),求树上长度为 \(k\) 的路径总数。
显然共有 \(n(n-1)\) 条路径,暴力统计复杂度过高。
考虑路径情况,发现我们可以将其分为两类:
- 经过根节点的路径。
- 不经过根节点的路径。
运用分治思想可以发现,我们可以只处理情况一,情况二直接分治下去即可。
至于根节点的选定之后再谈。
针对情况一,我们假设根节点为 \(p\),路径两端点为 \(x,y\),显然 \(x,y\) 属于 \(p\) 的不同子树。
利用这个思想可以进一步简化操作,只需要针对以 \(p\) 为根节点的树进行统计即可。
时间复杂度
显然时间复杂度与分治的次数相关,我们希望分治的次数尽量少。
这就要求以选定节点为根的子树层数尽量少,显然选取重心是最优的。
那么可以证明最大分治次数为 \(\log n\) 次。
在执行 \(Calc(p)\) 操作是我们通常有两种方法,各有优劣。
- \(O(n\log n)\):对空间要求较大,主要思想是用桶/树状数组来统计之前子树的答案,然后统计当前答案并不断刷新。(值得注意的是树状数组的常数与路径长度有关,可能常数较大)
- \(O(n \log^2 n)\):时间换空间,主要思想是将路径长度排序后利用单调性使用双指针等方法统计答案,显然空间与路径长度无关,更具有普适性。
还有就是最好不要在 \(Calc(p)\) 中使用 memset
来盲目初始化,这样会使时间复杂度大量增加。
代码
这里以模板题为例来讲解。
给定一棵有 \(n\) 个点的树,询问树上距离为 \(k\) 的点对是否存在。
不管怎么样,点分治的题目都是先找重心,显然树的重心可以用 dfs 简单解决。
void get_root(int u,int fa,int sum){// sum 是当前树的总结点数
sz[u]=1;//sz[i]记录以 u 为根的子树大小
mxp[u]=0;//mxp[i]记录以 i 节点为根的树所有的子树中最大的子树节点数
for(int i=head[u];i;i=ed[i].nxt){
int v=ed[i].to;
if(vis[v] || v==fa) continue;
get_root(v,u,sum);
sz[u]+=sz[v];
mxp[u]=max(mxp[u],sz[v]);
}
mxp[u]=max(mxp[u],sum-sz[u]);
if(!root || mxp[root]>mxp[u]) root=u;//更新
return;
}
//下面是调用
root=0;
get_root(u,0,sz[u]);
root_u=root;
然后是千篇一律的分治代码,同样用 dfs 解决,最大深度为 log n
层。
void dfs(int u){
vis[u]=true;
calc(u);
for(int i=head[u];i;i=ed[i].nxt){
int v=ed[i].to;
if(vis[v]) continue;
root=0;
get_root(v,0,sz[v]);
dfs(root);
}
return;
}
显然核心代码就是 \(Calc(u)\) 了,本题对空间要求不高,可以使用桶/双指针来通过,这里采用代码更长的双指针。
int dis[N],a[N],b[N],tot=0;
void get_dis(int u,int fa,int d,int anc){
dis[u]=d;
a[++tot]=u;
b[u]=anc;
for(int i=head[u];i;i=ed[i].nxt){
int v=ed[i].to,w=ed[i].val;
if(vis[v] || v==fa) continue;
get_dis(v,u,w+d,anc);
}
return;
}
bool cmp(int x,int y){
return dis[x]<dis[y];
}
void calc(int u){
tot=0;dis[u]=0;
a[++tot]=u;b[u]=u;
for(int i=head[u];i;i=ed[i].nxt){
int v=ed[i].to,w=ed[i].val;
if(vis[v]) continue;
get_dis(v,u,w,v);
}
sort(a+1,a+tot+1,cmp);//显然排序 O(n log n) 浪费了大部分的时间
for(int i=1;i<=m;i++){
if(ans[i]) continue;
int l=1,r=tot;
while(l<r){
int now=dis[a[l]]+dis[a[r]];
if(now<k[i]) ++l;
else if(now>k[i]) --r;
else if(b[a[l]]==b[a[r]]){
if(dis[a[r]]==dis[a[r-1]]) --r;
else ++l;
}
else{ans[i]=true;break;}
}
}
return;
}
综合起来就可以很好的通过本题,代码不再重复给出。
例题
例题一
统计一棵有 \(n\) 个节点的树中有多少条路径边权和 \(\leq k\)。
和模板题比较类似,只需要将桶改为树状数组统计即可,时间复杂度 \(O(n\log m)\)。
但是由于树状数组总值 \(m\leq 4\times 10^7\),时间复杂度远大于双指针的 \(O(n\log n)\),所以这里介绍第二种方法。
总体没有大变化,只是 \(Calc(p)\) 中有所改动。
考虑把树中的每一个点放入 \(a\) 数组中,同时预处理出 \(dis[i]\) 和 \(b[i]\),分别为节点 \(i\) 到根的距离,和所属的子树。
将 \(a\) 数组按照 \(dis\) 从小到大排序,用 \(L,R\) 两个指针分别从前、后扫描 \(a\) 数组。
根据单调性,只需要不断移动指针即可。
但是我们还需要排除两点在同一子树的情况,用 \(p[s]\) 维护在 \(L+1\) 到 \(R\) 之间满足 \(b[a[i]]=s\) 的节点数。
显然 \(R-L-p[b[a[i]]]\) 即为所求值。
例题二
统计树中有多少边权和为 \(3\) 的倍数的路径。
用桶分别维护不同余数的路径个数即可。
例题三
求一条简单路径,权值和等于 \(k\),且边的数量最小。
显然没有数量最小的条件就是模板题了,加了的话...用桶维护即可。
值得注意的是本题用 \(O(n\log^2 n)\) 的双指针做法只能得到 90 pts
,必须使用 \(O(n\log n)\) 的桶。
总结
点分治可以很好的解决树上路径统计问题,使用面较广,而且据说是近期省选热门考点。
仍需继续学习点分树。
完结撒花。