点分治
点分治
不是淀粉质。
概述
点分治适合处理大规模的树上路径信息问题。——摘自 OI Wiki
时间复杂度 \(O(n\log n)\) (每次 \(calc\) 时间复杂度为 \(O(size_{root})\))。
对于树上的所有路径及一个假定的根 \(rt\) ,有两种路径:
- 经过 \(rt\) 的;
- 不经过 \(rt\) 的。
第一种路径显然分两部分(可以为空),分别位于不同的子树中。
显然第二种路径对于某个节点 \(u\) ,属于第一种路径。所以分治解决即可。
点分治时间复杂度为 \(O(nT)\) ,其中 \(T\) 为树的深度。(因为每次分治要一共处理整棵树,而最多要进行 \(T\) 次分治)。当树是一条链且每次选择链头分治时,时间复杂度就是 \(O(n^2)\) 了。
因此,我们每次寻找子树的重心作为 \(rt\) 进行分治(重心满足最大子树 \(size\) 最小)。时间复杂度 \(O(nlogn)\) 。
该算法大致流程如下:
- \(rt\gets\) 树的重心。
- 计算经过 \(rt\) 的路径贡献。
- 对 \(rt\) 的每棵子树分别找重心,重复流程。
大致 Code(未过编)
#include<bits/stdc++.h>
#define ll long long
#define pf printf
#define sf scanf
using namespace std;
const int N=1e5+7;
int n,m;
int u,v,w;
int q;
int head[N],cnt;
int siz[N],mxsiz[N];//siz 即子树大小,mxsiz 以 u 为根最大子树 size
int rt;
bool de[N]; //点分治专属,是否删除
struct edge{
int to,val,ne;
}e[N<<1];
void add(int u,int v,int w){
e[++cnt].val=w,e[cnt].to=v,e[cnt].ne=head[u];
head[u]=cnt;
}
void getroot(int u,int fa){// 找根
siz[u]=1;// 初始化 size
mxsiz[u]=0;// 初始化 max_size
for(int i=head[u];i;i=e[i].ne){
int v=e[i].to,w=e[i].val;
if(de[v]||v==fa) continue;
getroot(v,u);
siz[u]+=siz[v];
mxsiz[u]=max(mxsiz[u],siz[v]);
}
mxsiz[u]=max(mxsiz[u],n-mxsiz[u]);//以 u 为根的子树包括上面那一堆
if(mxsiz[u]<mxsiz[rt]) rt=u;
}
void getans(int u,int fa,int ans,int from){// from 表示结点 u 来自哪棵子树
// 存储 ans
for(int i=head[u];i;i=e[i].ne){
int v=e[i].to,w=e[i].val;
if(v==fa||de[v]) continue;
getans(v,u,/*ans的一些操作*/,from);
}
}
void calc(int u){
for(int i=head[u];i;i=e[i].ne){//遍历每个子树
int v=e[i].to,w=e[i].val;
if(de[v]) continue;
getans(v,u,w,v);
}
// 对存储的 ans 进行一些操作
}
void solve(int u){ // 点分治
de[u]=1;
calc(u);// 计算经过 u 的路径的贡献
for(int i=head[u];i;i=e[i].ne){
int v=e[i].to,w=e[i].val;
if(de[v]) continue;
rt=0;// 初始化
n=siz[v];//当前要找重心的子树的总大小
getroot(v,u);
solve(rt);
}
}
int main(){
sf("%d%d",&n,&m);
for(int i=1;i<n;i++){
sf("%d%d%d",&u,&v,&w);
add(u,v,w),add(v,u,w);
}
sf("%d",&q);
mxsiz[0]=n;//初始化
getroot(1,0);
solve(rt);
pf("%d\n",ans);
}
补充
你会发现找重心的那段代码其实是错的。然而不影响时间复杂度正确,详见:
https://liu-cheng-ao.blog.uoj.ac/blog/2969
经验
P3806 模板
暴力思路
暴力枚举两个点,然后倍增求LCA求距离。时间复杂度 \(O(n^2logn)\) 。
点分治做法
参考该题解
暴力做法重复算了很多次距离。
以重心为根,计算每个点到根的距离,并排序。枚举每个点, \(logn\) 求出符合的第二个点的数量(代码中双指针 \(O(n)\) )。
这样就求出了经过根的所有答案。
对于不经过根的答案,拆分子树,找到子树的重心,递归下去。
因为每次找的是重心,所以一共要递归 \(logn\) 次,每次算距离共要遍历整棵树,即 \(n\) 个点,排序共 \(nlogn\) ,找第二个点双指针共 \(n\) ,答案处理共 \(nm\) ,因此算法总复杂度为 \(nlog^2n+nlogn+nmlogn=O(nlog^2n+nmlogn)\) 。
P4178 Tree
给定一棵 \(n\) 个节点的树,每条边有边权,求出树上两点距离小于等于 \(k\) 的点对数量。
做法时间复杂度 \(O(nlog^2n)\) 。
一眼点分治,这里解释 \(calc\) (计算经过结点 \(u\) 的路径贡献)部分。
将 \(u\) 子树下的所有点记录深度并排序,双指针即可,时间复杂度 \(nlogn\) 。
需要减去路径起点和终点经过 \(u\) 的同一棵子树的情况,只需要遍历 \(u\) 的儿子,分别减去 \(calc(v)\) 即可。
P4149 Race
给定一棵 \(n\) 个节点的树,每条边有边权,求出树上两点距离等于 \(k\) 的路径最小边数。
树上路径问题,用点分治。
时间复杂度 \(O(nlogn)\) 。
计算每个结点的贡献:\(clac\) 中用桶记录每个深度的最小边数即可,然后清空桶(不可 memset)。
其他和模板一样。
求第 k 长路径
给定一棵树,求树上第 k 长路径的长度。\(n,m\le 10^5\)。
考虑二分答案,二分长度 \(len\) 然后点分治求长度 \(\le len\) 的路径数量。时间复杂度 \(O(n\log^2n\log V)\)。
优化:发现每次点分治的过程都是一样的,没必要做 \(\log V\) 次。考虑到每次求经过重心的路径的方法是先求 \(calc(rt)\) 然后减去所有儿子的答案(减去重复经过一个儿子的答案)。开一个结构体记录点分治的过程,vector d_u;
记录经过点 \(u\) 的所有 \(dis\)。设 \(u\) 的子树大小为 \(size\),显然 vector 的空间为 \(size\)。对于每个点,我们要记录它自己和它的儿子,所以一共要开 \(2n\) 个 vector。
然后每次二分 \(O(n\log n)\) 统计长度 \(\le mid\) 的边数即可。
总时间复杂度 \(O(n\log n \log V )\)。
点分树
不是淀粉树。
点分治可以处理树上路径问题,其本质大概是对于每层的若干个重心求经过重心的其点分治子树的答案。
如果要求经过一个点的路径信息,暴力枚举端点是 \(O(n^2)\) 的,使用点分治做是 \(O(n \log n)\) 的。
也许算板子题:P6329 【模板】点分树 | 震波
给你一棵树,每个点有点权。有 \(q\) 次询问,每次问于一个点 \(u\) 距离不超过 \(k\) 的点的点权之和是多少,或者修改一个点的点权。
对于一次询问,考虑怎么做点分治。
对于两个端点都在询问的这个点的点分治子树内的情况是好做的。对于一个端点在这个点的点分治子树外的情况,也就是经过其点分治祖先的情况,可以采用容斥的方法,由于询问的点和其点分治祖先的距离可以预处理,因此我们只需要求出距离其点分治祖先一定的点的点权之和,容斥除去其点分治祖先在询问的点的方向的子树的答案即可。
但是有多次询问,不能每次都做点分治。
如果没有修改操作就是完全是板子的点分树了。考虑把点分治的过程记录下来,每个重心向其点分治子树的重心连边,建出点分树。我们需要存每个重心的点分治子树的点到重心的距离为 \(i\) 的点权之和,以及距离其点分治父亲为 \(i\) 的点权之和,以方便做容斥。并且做个前缀和。对于每个询问,计算答案,先算上它的点分治子树内的答案,然后枚举它在点分树上的所有祖先(一共 \(\log\) 个),答案加上目前的祖先点分治子树内与目前的祖先距离一定深度的点的点权之和,以及除去包含它的那个点分治子树的答案。时间和空间都是 \(O(n\log n)\)。
有修改操作不难,每个点的桶改成动态开点的树状数组就可以了。每次修改也是只需要改 \(\log\) 个点分治重心。时间多一个 \(\log\)。
时间是 \(O(n \log^2 n)\) 的,空间是 \(O(n \log n)\) 的。
对我来说略微难写。
本文来自博客园,作者:liyixin,转载请注明原文链接:https://www.cnblogs.com/liyixin0514/p/18357756