点分治及题目

点分治

一般可以用于处理大规模树上路径问题

既然是处理路径问题,那么可以把路径分成两种,经过当前根节点的路径,不经过当前根节点的路径
处理完经过当前根节点的路径,然后删掉根节点,此时肯定会形成一个或多个子树,那么剩下的不经过当前根节点的路径,递归到这些子树中处理
删掉的节点肯定在接下来的处理中就不会被考虑了,这就意味着并不会有路径被重复统计

然而如果图是一个链,那么一共要处理 (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=sum_{j=1}^n s(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}),这点挺好理解

code

P4149 [IOI2011]Race

here
给一棵树,每条边有权。求一条简单路径,权值和等于 (k),且边的数量最小。

裸的很,就维护距离的同时维护一个 (num_i) 表示距离为 (i) 的点,最少经过几条边
然后把 (tmp)(judge) 里存的时候,再维护一个 (numed),维护的是当前根的所有子树的 (num),然后清空 (num) 数组,更新答案
code

原文地址:https://www.cnblogs.com/suxxsfe/p/12870120.html