suxxsfe

一言(ヒトコト)

点分治及题目

点分治

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

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

然而如果图是一个链,那么一共要处理 \(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(\text{每次处理的复杂度}\log n)\)

可以从这里看树的重心的其他性质的证明:https://www.cnblogs.com/suxxsfe/p/13543253.html

模板题:P3806 【模板】点分治1
给定一棵有 \(n\) 个点的树,\(m\) 次询问树上距离为 \(k\) 的点对是否存在。
\(k\le 10^7,n\le 10^4,m\le 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(nm\log 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\n",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\) 的点对数量。
\(n\le 4\cdot 10^4,k\le 2\cdot 10^4\)

一开始想的是对每个 \(k'\le k\),按上个题求出长度恰好等于 \(k'\) 的答案,但是复杂度不行
其实要把 \(judge\) 数组改成线段树,把每个 \(tmp_i\) 作为下标单点加一下就好,然后清空的时候是单点赋值,对于区间 \([0,k-tmp_i]\) 用一个区间求和加到答案里就行了
code

P2634 [国家集训队]聪聪可可

here
树上任选两点,求这两点间距离是 \(3\) 的倍数的概率是多少

问题转化为求有多少对点,之间的距离是 \(3\) 的倍数,然后除以 \(n^2\)
就是把CF那题放在 \(\bmod 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\)
则对答案的贡献是 \(num\cdot (size_{root}-size_x)-\sum color\_sum\),此处 \(x\) 是当前子树的根
不难理解,\(\sum color\_sum\) 意思就是 \(u\) 想要到达根,就要经过这些颜色,所以再在其它子树中经过,就不能再算了,贡献不能计算进去
而一共有 \(size_{root}-size_x\) 个点可以到达,没到达一个点,在从 \(u\) 到根的路径中,都有 \(num\) 个不同颜色为答案产生贡献,所以加上 \(num\cdot (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

posted @ 2020-05-11 16:49  suxxsfe  阅读(215)  评论(0编辑  收藏  举报