点分治
一般可以用于处理大规模树上路径问题
既然是处理路径问题,那么可以把路径分成两种,经过当前根节点的路径,不经过当前根节点的路径
处理完经过当前根节点的路径,然后删掉根节点,此时肯定会形成一个或多个子树,那么剩下的不经过当前根节点的路径,递归到这些子树中处理
删掉的节点肯定在接下来的处理中就不会被考虑了,这就意味着并不会有路径被重复统计
然而如果图是一个链,那么一共要处理 (n) 个根节点,每次的处理一般不会低于 (O(n)),那么复杂度大于 (O(n^2)) 不够优秀
所以在选取根节点的时候,要选取当前子树的重心,即当前子树中,最大的子树最小的一个节点
重心的性质:以重心为根,重心的每个子树的大小,都不会超过整个树大小的一半,证明一下
- 假设重心是 (u),它的一个子树大小超过整个树大小的一半,设这个子树的根是 (v)(与 (u) 相邻)
- 用 (size_i) 表示以 (i) 为根的子树大小,则 (size_v>frac{size_u}{2})
- 那么,(u) 除了子树 (v) 以外的其它所有子树(把 (u) 本身也计算在内)的大小是 (size_u-size_v)
- 所以,如果以 (v) 为重心,则它的一个子树是 (size_u-size_v),这个子树就是以 (u) 为根的那个。(size_u-size_v<size_v),此时,可以说明以 (u) 为根的那个子树,就是以 (v) 的所有子树中大小最大的那个,同时,也说明如果以 (v) 为重心,最大子树的大小小于以 (u) 为根最大子树的大小,则矛盾。得证
因此,最多把整个树递归 (log n) 层即可,然后可以类比在数列上的分治,同一递归深度处理的 总复杂度 和 (n) 有关,并且都相同,则复杂度是 (O( ext{每次处理的复杂度}log n))
可以从这里看树的重心的其他性质的证明:https://www.cnblogs.com/suxxsfe/p/13543253.html
模板题:P3806 【模板】点分治1
给定一棵有 (n) 个点的树,(m) 次询问树上距离为 (k) 的点对是否存在。
(kle 10^7,nle 10^4,mle 100)
离线做,考虑能不能 (O(n)) 求出对于每一个询问,有没有一条符合要求的穿过当前根节点的路径
枚举当前根的每一个子树,算出子树中每一个点距离根的距离,存入 (tmp) 数组,看一看之前是不是存在一个长度为 (k-tmp_i) 的路径
如何判断是否存在?用一个 (judge) 数组,(judge_i) 表示目前有没有一条从某个已经访问过的子树到根节点的,长度为 (i) 的路径
就判断 (judge_{k-tmp_i}) 就行,当 (tmp) 所有元素判断完之后,对于所有元素 judege[tmp[i]]=1
这样就能统计到所有的路径,同时又不会出现路径”折返“的情况,就是两个在同一子树中的节点被计算(因为他们的路径不会经过当前的根)
然后每个 (tmp) 中的距离都要存一下,用来清空 (judge),不能 memset
找重心的过程用 find
函数实现,注意要调用两遍,原因在注释里
复杂度 (O(nmlog n))
#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cmath>
#include<map>
#include<iomanip>
#include<cstring>
#define reg register
#define EN std::puts("")
#define LL long long
inline int read(){
register int x=0;register int y=1;
register char c=std::getchar();
while(c<'0'||c>'9'){if(c=='-') y=0;c=std::getchar();}
while(c>='0'&&c<='9'){x=x*10+(c^48);c=std::getchar();}
return y?x:-x;
}
#define N 10006
#define M 20006
struct graph{
int fir[N],nex[M],to[M],w[M],tot;
inline void add(int u,int v,int z){
to[++tot]=v;w[tot]=z;
nex[tot]=fir[u];fir[u]=tot;
}
}G;
int n,m,sum,root;//sum 记录当前树的大小,root 记录当前找到的重心
int ask[106],ans[106];
int size[N],max[N],vis[N];//vis 记录一个节点是不是已经被删除
int tmp[N],dis[N],que[N];
bool judge[10000006];
void find(int x,int fa){
size[x]=1;max[x]=0;//函数会多次调用,所以这里要初始化
for(reg int v,i=G.fir[x];i;i=G.nex[i]){
v=G.to[i];
if(v==fa||vis[v]) continue;
find(v,x);
size[x]+=size[v];
if(max[x]<size[v]) max[x]=size[v];
}
if(max[x]<sum-size[x]) max[x]=n-size[x];
if(max[x]<max[root]) root=x;
}
void get_dis(int u,int fa){
tmp[++tmp[0]]=dis[u];
for(reg int v,i=G.fir[u];i;i=G.nex[i]){
v=G.to[i];
if(v==fa||vis[v]) continue;
dis[v]=dis[u]+G.w[i];
get_dis(v,u);
}
}
inline void calc(int u){
que[0]=0;
for(reg int v,i=G.fir[u];i;i=G.nex[i]){
v=G.to[i];
if(vis[v]) continue;
tmp[0]=0;dis[v]=G.w[i];
get_dis(v,u);
for(reg int j=1;j<=tmp[0];j++){
for(reg int k=1;k<=m;k++)//遍历询问
if(ask[k]>=tmp[j]) ans[k]|=judge[ask[k]-tmp[j]];
}
for(reg int j=1;j<=tmp[0];j++)if(tmp[j]<=1e7){
//更改 judge,为后面的子树的查询使用,同时加入队列
judge[tmp[j]]=1;que[++que[0]]=tmp[j];
}
}
for(reg int i=1;i<=que[0];i++) judge[que[i]]=0;
}
void divide(int u){
// std::printf("now : %d
",u);
vis[u]=judge[0]=1;
calc(u);
for(reg int v,i=G.fir[u];i;i=G.nex[i]){
v=G.to[i];
if(vis[v]) continue;
root=0;sum=size[v];
find(v,0);find(root,0);
divide(root);
}
}
int main(){
n=read();m=read();
for(reg int u,v,z,i=1;i<n;i++){
u=read();v=read();z=read();
G.add(u,v,z);G.add(v,u,z);
}
for(reg int i=1;i<=m;i++) ask[i]=read();
max[0]=sum=n;
find(1,0);find(root,0);
//此处要调用两遍,因为在 divide 函数中,要用到 size[v]
//而如果不以 root 为再调用一遍,size[v] 的值就是错的(以 1 为根,而实际上应该是以 root 为根)
//所以 divide 里再找到的重心也就是错的
divide(root);
for(reg int i=1;i<=m;i++) std::puts(ans[i]?"AYE":"NAY");
return 0;
}
CF161D Distance in Tree
传送门
输入点数为 (n) 一棵树
求树上长度恰好为 (k) 的路径个数
和模板几乎一样,就把 (judge) 原来的记录”有没有“,改成记录”有几条”就行了
代码
P4178 Tree
here
给定一棵 (n) 个节点的树,每条边有边权,求出树上两点距离小于等于 (k) 的点对数量。
(nle 4cdot 10^4,kle 2cdot 10^4)
一开始想的是对每个 (k'le k),按上个题求出长度恰好等于 (k') 的答案,但是复杂度不行
其实要把 (judge) 数组改成线段树,把每个 (tmp_i) 作为下标单点加一下就好,然后清空的时候是单点赋值,对于区间 ([0,k-tmp_i]) 用一个区间求和加到答案里就行了
code
P2634 [国家集训队]聪聪可可
here
树上任选两点,求这两点间距离是 (3) 的倍数的概率是多少
问题转化为求有多少对点,之间的距离是 (3) 的倍数,然后除以 (n^2)
就是把CF那题放在 (mod 3) 意义下,注意在这里 ((u,v),(v,u)) 算两个,且 ((u,u)) 也算,那么答案要乘二再加 (n)
code
P2664 树上游戏
here
前面几个都太水了,这个稍微有点难度
一棵树,树的每个节点有个颜色。给一个长度为 (n) 的颜色序列,定义 (s(i,j)) 为 (i) 到 (j) 的颜色数量。以及
求出所有的 (sum_i)
用点分治以后,问题变成如何求出以根为 LCA 的点对的答案贡献
如果有一个点的颜色,在从根到它的路径上是第一次出现,那么他(设为 (u))可以为根的 其它 子树的答案都贡献上 (size_u)
因为其它的节点都可以有一个到他子树的路径,会经过他,而他的颜色又是第一次出现
设 (all=sum size_u)
再设 (color\_sum_i),为颜色 (i) 对 (all) 的贡献的总值是多少,就是所有颜色在从根到它的路径上是第一次出现,并且颜色是 (i) 的节点的 (size) 之和
我们可以通过一遍 dfs 来求出上面的信息
然后枚举每一个子树,尝试求出这个子树中所有点的答案
首先肯定要把这个子树对 (all,color\_sum) 的贡献都去掉,因为上面说了这是对其它子树答案的贡献
去掉的过程稍有麻烦,具体看代码能想明白
然后再来一遍 dfs,做出答案
我们设 (num) 为 (u) 到根路径上的不同的颜色数,(all') 是 (all)(减去此子树贡献的)减掉从 (u) 到路径上每个不同颜色,的 (color\_sum) 和
则对答案的贡献是 (numcdot (size_{root}-size_x)-sum color\_sum),此处 (x) 是当前子树的根
不难理解,(sum color\_sum) 意思就是 (u) 想要到达根,就要经过这些颜色,所以再在其它子树中经过,就不能再算了,贡献不能计算进去
而一共有 (size_{root}-size_x) 个点可以到达,没到达一个点,在从 (u) 到根的路径中,都有 (num) 个不同颜色为答案产生贡献,所以加上 (numcdot (size_{root}-size_x))
最后做完这遍 dfs 要把刚才去掉的贡献再加回去
然后枚举完清零,也是 dfs 实现
根节点的答案单独计算,是 (all-color\_sum(color_{root})+size_{root}),这点挺好理解
P4149 [IOI2011]Race
here
给一棵树,每条边有权。求一条简单路径,权值和等于 (k),且边的数量最小。
裸的很,就维护距离的同时维护一个 (num_i) 表示距离为 (i) 的点,最少经过几条边
然后把 (tmp) 往 (judge) 里存的时候,再维护一个 (numed),维护的是当前根的所有子树的 (num),然后清空 (num) 数组,更新答案
code