初学点分治(静态)
引入
我们有时候会遇到这样一些题目:
给定一颗树(一般是无根的),然后多次询问你任意两点之间的信息。如果我们直接暴力,时间复杂度很明显是O(n*m)的。
m次的查询很明显是无法优化的,那么我们考虑优化一下n。是否能做到预处理后O(logn*m)或者直接O(m*1)的时间复杂度呢?或者换个说法,我们能不能提前把两点之间的信息先处理出来以供查询?
现在我们提供一种思想,就是把树拆开。
怎么拆?
原理及实现
树的分治,就是把树从一个完整的树分成大大小小的子树来统计。
比如
我们先处理好以A为根节点的点对
即每一个点到点A的距离,那么以B,E为例,它们之间的距离就是BA到EA的距离之和。
然后去掉A,剩下B和E为根的树,重复以上步骤,直到所有点都作为根被遍历过。
实现步骤如下
- 寻找树的重心
因为这是一颗无根树,所以我们考虑一下选定一个根。
而树的dfs一般都是按深度搜下去的,我们自然希望深度最小以保证时间复杂度。
举个例子:
给你一条链,你是每次从链首一个一个拆开好还是从中间往两边扩展好?
答案很明显是从中间好,因为链首的复杂度是O(n)的,链中间是O(logn)的。
那么我们要怎么寻找呢?
我们假定一个点,搜出它的所有子树的大小,并从中找到最大的一个,与总点数减去这个点的子树和相比较、
为什么?因为树的重心定义如下:
找到一个点,其所有的子树中最大的子树节点数最少,那么这个点就是这棵树的重心,删去重心后,生成的多棵树尽可能平衡。
void getroot(int x,int fa)
{
son[x]=1;
int ret=0;
for(int i=head[x];i;i=e[i].next)
{
int to=e[i].to;
if(!vis[to]&&to!=fa)
{
getroot(to,x);
son[x]+=son[to];
ret=max(ret,son[to]);
}
}
ret=max(ret,size-son[x]); if(ret<minn)minn=ret,root=x;
}
- 根据容斥原理处理出点之间的信息
上面已经提到,我们处理点之间的信息,是处理这个点到根节点的信息。
再以这张图为例
因为是直接递归,而树并不知道那个点是它的左子树,哪个是右子树,
所以会有一下路径别处理出来:
A—>A
A—>B
A—>B—>C
A—>B—>D
A—>E
A—>E—>F
那么我们在合并答案是会将上述6条路径两两进行合并。
但是按A—>B—>C 和 A—>B—>D
我又要求B->C的距离,那信息岂不是A->B->C+A->B,这显然是不行的
减去重边,就是减去每个子树的单独贡献。
例如对于以B为根的子树,就会减去:
B—>B
B—>C
B—>D
这个要结合代码来体现。
(注:此代码来至洛谷点分治模版)
void dep(int x,int fa,int v) //求出当前点为根时,它的子树信息
{
vi[++tot]=v;
for(int i=head[x];i;i=e[i].next)
{
int to=e[i].to;
if(to!=fa&&!vis[to])
dep(to,x,v+e[i].v);
}
}
void cal(int x,int f,int v) //根据容斥原理计算子树信息
{
tot=0; dep(x,x,0);
for(int i=1;i<=tot;i++)
for(int j=1;j<=tot;j++) //一般这个统计格式适用于大多数题目。
if(f&&vi[i]+vi[j]<=k)
num[vi[i]+vi[j]]++;
else if(vi[i]+vi[j]+v<=k)
num[vi[i]+vi[j]+v]--;
}
void dfs(int x)
{
cal(x,1,0); vis[x]=1; //计算每一个点到根的贡献
for(int i=head[x];i;i=e[i].next)
{
int to=e[i].to;
if(vis[to])continue;
cal(to,0,e[i].v*2); //减去重边,注意,这里的边值是被计算了两次的,要*2
minn=n,size=son[to];
getroot(to,x); dfs(root);
}
}
好像已经没什么好讲的了。