点分治

点分治及其应用

思想

先说一下点分治的基本思想:选择树上一个点作为分治中心,为了保证复杂度,选择的点有一些特殊的要求。

接下来,把原问题分解成几个相同的子问题,进行递归解决。

一般地,我们假设当前根节点为 rt,所以我们要统计的路径必然满足以下二者之一:

  • 经过 rt

  • 不经过 rt,就是在 rt 的子树上。

树的重心

上面说到,为了保证时间复杂度,我们选择的点有一些特殊要求。一般地,我们选择的点为树的重心。因为这样剩下的子树的最大大小不超过整棵树大小的一半,所以这样递归层数为 O(logn) 级别的。

为了帮助读者更好的理解上文,这里放一下查找树的重心的代码:

void get_root(int u,int f){
	siz[u]=1;
	mx[u]=0;
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(j==f||st[j])continue;
		get_root(j,u);
		siz[u]+=siz[j];
		mx[u]=max(mx[u],siz[j]);
	}
	mx[u]=max(mx[u],sum-siz[u]);
	if(mx[u]<mx[rt])rt=u;//根据重心的定义找重心 
}

点分治1

算法:点分治,树的重心。

这道例题比较简单,但是实测如果每次都跑一次点分治的话时间不能接受,所以我们考虑把询问离线下来,然后做一次点分治处理所有操作。

下面说一下这道题的做法:

考虑路径是否经过根 rt,不经过的我们递归处理,这里考虑经过的怎么做。

比较显然地,如果在一个子树中距离 rt 长为 l 的链,并且距离 rt 长为 kl 的链在其他子树中出现,那么长为 k 的链一定存在。

并且,两者的出现顺序对于我们的答案没有影响,于是我们便可以一棵一棵子树维护,具体分为下面几步:

  • dfs 处理当前子树上每个点与根 rt 的距离。

  • dfs 时,记录哪些长度的链出现过。

  • 与之前已经统计过的子树的数据相结合,并且进行更新。

  • 清空当前子树的边的信息,这里为了保证时间复杂度,不能使用 memset

  • 最后递归进入子树进行点分治。

有一些小细节和注释,放在了下面的代码中:

#include<bits/stdc++.h>
#define int long long
#define N 100005
#define M 200005
#define K 10000005
#define inf 2e18
using namespace std;
int n,m,h[N],e[M],w[M],ne[M],idx;
int qs[N],res[N],rt,sum,siz[N],mx[N];
int st[N],tf[K],dis[N],d[N],dcnt,q[N],hh,tt;
/*
第一行基础变量和链式前向星存图
qs 为每个询问,res 为询问结果
sum 子树总大小,rt 当前树根
mx 最大子树大小,st 当前点是否被处理过
tf 是否有某一长度的链,dis 距离根的距离
d 当前子树的链的长度,dcnt 当前子树到根的链的个数
q,hh,tt 队列 
*/
void add(int a,int b,int c){
	e[idx]=b;w[idx]=c;ne[idx]=h[a];h[a]=idx++;
}
void get_root(int u,int f){
	siz[u]=1;
	mx[u]=0;
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(j==f||st[j])continue;
		get_root(j,u);
		siz[u]+=siz[j];
		mx[u]=max(mx[u],siz[j]);
	}
	mx[u]=max(mx[u],sum-siz[u]);
	if(mx[u]<mx[rt])rt=u;//根据重心的定义找重心 
}
void get_dist(int u,int f){
	d[++dcnt]=dis[u];//当前的距离存入可能的长度 
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(j==f||st[j])continue;
		dis[j]=dis[u]+w[i];
		get_dist(j,u);
	}
}
void solve(int u,int f){
	hh=0;tt=-1;
	q[++tt]=0;
	tf[0]=1;st[u]=1;//当前点已经处理过,且长度为0的链一定存在(即不选) 
	for(int i=h[u];~i;i=ne[i]){
		int v=e[i];
		if(v==f||st[v])continue;
		dis[v]=w[i];
		get_dist(v,u);
		for(int k=1;k<=dcnt;k++){//链长度 
			for(int j=1;j<=m;j++){//询问 
				if(qs[j]>=d[k]){
					res[j]|=tf[qs[j]-d[k]];//如果d[k]和qs[j]-d[k]都出现,就存在 
				}
			}
		}
		for(int j=1;j<=dcnt;j++){
			if(d[j]<10000005){//如果链长度有意义(可能被询问) 
				q[++tt]=d[j];//就加到队列里 
				tf[d[j]]=1;//这个长度能被凑出来 
			}
		}
		dcnt=0;
	}
	while(hh<=tt)tf[q[hh++]]=0;//清空之前子树的信息 
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];//继续找其他子树计算 
		if(j==f||st[j])continue;
		sum=siz[j];rt=0;
		mx[rt]=inf;
		get_root(j,u);
		get_root(rt,-1);
		solve(rt,u);
	}
}
signed main(){
	cin>>n>>m;
	memset(h,-1,sizeof h);
	for(int i=1;i<n;i++){
		int a,b,c;
		cin>>a>>b>>c;
		add(a,b,c);add(b,a,c);
	}
	for(int i=1;i<=m;i++){
		cin>>qs[i];
	}
	rt=0;mx[rt]=inf;sum=n;
	get_root(1,-1);
	get_root(rt,-1);
	solve(rt,-1);
	for(int i=1;i<=m;i++){
		if(res[i])cout<<"AYE\n";
		else cout<<"NAY\n";
	}
	return 0;
}

点分治上面的三个函数比较模板化,最多只需要微调,建议理解性背诵,而 solve 函数需要根据题目来推,相对不需要过多记忆。

Tree

算法:点分治,树的重心,双指针。

根据上面的模板题,这道题的思路还是比较好得出的。

我们考虑一个事情,暴力维护根 rt 到其他点的距离,但这样时间复杂度过高,需要优化。

我们还是考虑和上一题一样,把不经过根 rt 的路径进行递归处理,剩下的路径拆成两半计算,这两半在 rt 不同的子树内。

那我们如何判断两条路径在不在同一棵子树内呢?可以记录每个点 x 所属的子树 bx,特别地,brt=rt

接着我们记录所有点到根 rt 的距离,并且从小到大排序,得到数组 subt

然后我们采用双指针 l,r 进行扫描,由于已经排序,所以我们这样写是正确的。

但是,我们会发现当前的 [l+1,r] 区间内的路径有可能与 subtl 有相同的根 x,所以我们还需要记录 [l+1,r] 中以 x 为根的点的数量。

综上,我们最后产生的贡献为 rlcntsubtl,其他部分与上面的模板题基本一致,具体实现可看下面的代码。

#include<bits/stdc++.h> 
#define int long long
#define N 100005
#define M 200005
#define inf 2e18
using namespace std;
int n,m,h[N],e[M],w[M],ne[M],idx;
int siz[N],mx[N],rt,sum,st[N],dis[N];
int b[N],subt[N],scnt[N],dcnt,res;
bool cmp(int a,int b){
	return dis[a]<dis[b];
}
void add(int a,int b,int c){
	e[idx]=b;w[idx]=c;ne[idx]=h[a];h[a]=idx++;
}
void get_root(int u,int f){
	siz[u]=1;
	mx[u]=0;
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(j==f||st[j])continue;
		get_root(j,u);
		siz[u]+=siz[j];
		mx[u]=max(mx[u],siz[j]);
	}
	mx[u]=max(mx[u],sum-siz[u]);
	if(mx[u]<mx[rt])rt=u;
}
void get_dist(int u,int f){
	if(f!=rt)b[u]=b[f];
	else b[u]=u;
	subt[++dcnt]=u;scnt[b[u]]++;
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(j==f||st[j])continue;
		dis[j]=dis[u]+w[i];
		get_dist(j,u);
	}
}
void solve(int u,int f){
	subt[++dcnt]=u;b[u]=u;dis[u]=scnt[u]=0;
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(j==f||st[j])continue;
		dis[j]=w[i];
		scnt[j]=0;
		get_dist(j,u);
	}
	sort(subt+1,subt+dcnt+1,cmp);
	int l=1,r=dcnt;
	st[u]=1;
	while(l<r){
		while(dis[subt[l]]+dis[subt[r]]>m){
			scnt[b[subt[r]]]--;
			r--;
		}
		res+=r-l-scnt[b[subt[l]]];
		l++;
		scnt[b[subt[l]]]--;
	}
	dcnt=0;
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(j==f||st[j])continue;
		sum=siz[j];
		rt=0;
		mx[rt]=inf;
		get_root(j,u);
		get_root(rt,-1);
		solve(rt,u);
	}
}
signed main(){
	cin>>n;
	memset(h,-1,sizeof h);
	for(int i=1;i<n;i++){
		int a,b,c;
		cin>>a>>b>>c;
		add(a,b,c);add(b,a,c);
	}
	rt=0;
	mx[rt]=inf;
	sum=n;
	get_root(1,-1);
	get_root(rt,-1);
	cin>>m;
	solve(rt,-1);
	cout<<res;
	return 0;
}

聪聪可可

算法:点分治,数学。

放在前面:这个题可以用树形 dp 来写,但是在这里不再赘述。

这里对路径的分类方式仍然很套路,和上面的分类方式是相同的。

考虑这两种链出现什么情况会满足题意:

  • 两边链的长度都是 3 的倍数。

  • 一边链的长度模 31,一边链的长度模 32

根据这个,我们可以比较自然的想到统计每棵子树上与根 rt 距离模 3 分别余 0,1,2 的点的个数 b0,b1,b2

为了方便计算,我们在设 db0,db1,db2 为前面的子树上与根 rt 距离模 3 分别余 0,1,2 的点的个数之和。

所以我们的贡献为 b0×db0+b1×db2+b2×db1.

最后我们在十分套路地把 b 合并入 db,然后递归子树进行点分治。

需要注意的是,点对是有序的,故我们统计的答案需要变成原来的 2 倍。同样,形如 (a,a) 的点对也符合要求,故答案要再加上点的个数。

下面给出这道题的代码:

#include<bits/stdc++.h>
#define int long long
#define N 100005
#define M 200005
#define inf 2e18
using namespace std;
int n,h[N],e[M],w[M],ne[M],idx;
int st[N],b[5],db[5],res,rt,sum;
int siz[N],mx[N],dis[N],dcnt;
int gcd(int a,int b){
	return b?gcd(b,a%b):a;
}
void add(int a,int b,int c){
	e[idx]=b;w[idx]=c;ne[idx]=h[a];h[a]=idx++;
}
void get_root(int u,int f){
	siz[u]=1;
	mx[u]=0;
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(j==f||st[j])continue;
		get_root(j,u);
		siz[u]+=siz[j];
		mx[u]=max(mx[u],siz[j]);
	}
	mx[u]=max(mx[u],sum-siz[u]);
	if(mx[u]<mx[rt])rt=u;
}
void get_dist(int u,int f){
	b[dis[u]%3]++;
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(j==f||st[j])continue;
		dis[j]=dis[u]+w[i];
		get_dist(j,u);
	}
}
void solve(int u,int f){
	st[u]=1;
	db[0]=db[1]=db[2]=0;
	dis[u]=0;
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(j==f||st[j])continue;
		b[0]=b[1]=b[2]=0;
		dis[j]=w[i];
		get_dist(j,u);
		res+=b[0]*db[0] +b[1]*db[2]+b[2]*db[1];
		db[0]+=b[0];db[1]+=b[1];db[2]+=b[2];
	}
	res+=db[0];
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(j==f||st[j])continue;
		sum=siz[j];
		rt=0;
		mx[rt]=inf;
		get_root(j,u);
		get_root(rt,-1);
		solve(j,u);
	}
}
signed main(){
	cin>>n;
	memset(h,-1,sizeof h);
	for(int i=1;i<n;i++){
		int a,b,c;
		cin>>a>>b>>c;
		add(a,b,c);add(b,a,c);
	}
	sum=n;
	rt=0;
	mx[rt]=inf;
	get_root(1,-1);
	get_root(rt,-1);
	solve(rt,-1);
	res=res*2+n;
	int g=gcd(res,n*n);
	cout<<res/g<<'/'<<n*n/g;
	return 0;
}

树上游戏

算法:点分治。

困难题。这道题的询问比较显然地引导我们使用点分治。所以我们考虑怎么使用。

首先我们把路径划分成两种,经过根的与不经过根的,我们只考虑经过根的,另一种通过递归解决。

经过根的路径又可以分成两种,以根为一个端点的与不以根为端点的。

我们先考虑以根为端点的路径,如果一个颜色在这条路径上第一次出现,那么我们考虑这条路径的可能条数一定是 sizu,这里指 u 的子树大小。这个还是比较好理解的,就不再过多解释了。

接下来考虑另一种情况,比较套路地,我们把uv 的路径拆成两半,分别为 rtu,rtv,这里的 rt 为树根。

下文中的 sonu 所在子树的根。

先考虑 rtu 的部分的贡献,可以发现,如果一个路径在 rtu 上第一次出现,那么他会造成贡献。这时我们看 v 的可能点数,显然为 sizrtsizson,于是贡献为上文提到的那个东西乘上路径上的颜色个数。

再考虑 rtv 的部分的贡献,当然这里要先把 son 所在子树的东西清掉。具体地,如果发现一个颜色在 rtu 上第一次出现,把他对根的贡献清掉。

同样,如果在 rtv 的路径上的某一个颜色在 rtu 上出现过,那么这个颜色一定在计算 rtu 的贡献是被算过了,所以我们也要把这个颜色的贡献清掉。

上述过程均可以使用 dfs 完成,在统计完贡献后记得还原子树 son 的数据。

最后放一下这道题的代码:

#include<bits/stdc++.h>
#define int long long
#define N 100005
#define M 200005
#define inf 2e18
using namespace std;
int n,m,h[N],e[M],ne[M],idx;
int rt,sum_siz,col[N],siz[N],mx[N];
int delta,num,sum,cnt[N],col_siz[N],res[N];
bool st[N];
void add(int a,int b){
	e[idx]=b;ne[idx]=h[a];h[a]=idx++;
}
void get_root(int u,int f){//经典的板子,找重心代码 
	siz[u]=1;
	mx[u]=0;
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(j==f||st[j])continue;
		get_root(j,u);
		siz[u]+=siz[j];
		mx[u]=max(mx[u],siz[j]);
	}
	mx[u]=max(mx[u],sum_siz-siz[u]);
	if(mx[u]<mx[rt])rt=u;
}
void dfs1(int u,int f){//算其中一端为根的贡献 
	siz[u]=1;
	cnt[col[u]]++;//把这个颜色加上 
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(j==f||st[j])continue;//板子 
		dfs1(j,u);
		siz[u]+=siz[j];
	}
	if(cnt[col[u]]==1){//如果只出现一次,那么这个点会产生贡献 
		sum+=siz[u];//这个点下面的路径共有siz_u条 
		col_siz[col[u]]+=siz[u];//这个颜色的贡献也要记得修改 
	}
	cnt[col[u]]--;//回溯时把这个颜色去掉 
}
void modify(int u,int f,int val){//修改子树的贡献 
	cnt[col[u]]++;//同上 
	if(cnt[col[u]]==1){
		sum+=val*siz[u];//看情况是增加还是去掉 
		col_siz[col[u]]+=val*siz[u];
	}
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(j==f||st[j])continue;
		modify(j,u,val);
	}//这部分代码基本上等同于dfs1,只是看是增加贡献还是减少贡献 
	cnt[col[u]]--;
}
void dfs2(int u,int f){//这里计算链rt->u和rt->v的贡献 
	cnt[col[u]]++;//同上 
	if(cnt[col[u]]==1){
		sum-=col_siz[col[u]];//把u所在子树的贡献去掉 
		num++;
	}
	res[u]+=sum+num*delta;
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(j==f||st[j])continue;
		dfs2(j,u);
	}
	if(cnt[col[u]]==1){
		sum+=col_siz[col[u]];//回溯时再加回来 
		num--;
	}
	cnt[col[u]]--;
}
void init(int u,int f){
	cnt[col[u]]=col_siz[col[u]]=0;//清空
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(j==f||st[j])continue;
		init(j,u);
	}
}
void solve(int u,int f){
	st[u]=1;
	dfs1(u,f);
	res[u]+=sum;
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(j==f||st[j])continue;
		cnt[col[u]]++;
		sum-=siz[j];col_siz[col[u]]-=siz[j];
		modify(j,u,-1);//去掉u子树的贡献 
		cnt[col[u]]--;
		delta=siz[u]-siz[j];
		dfs2(j,u);
		cnt[col[u]]++;
		sum+=siz[j];col_siz[col[u]]+=siz[j];
		modify(j,u,1);//再加回来 
		cnt[col[u]]--;
	}
	sum=num=0;
	init(u,f);
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(j==f||st[j])continue;
		sum_siz=siz[j];
		rt=0;mx[rt]=inf;
		get_root(j,u);
		get_root(rt,-1);
		solve(rt,-1);
	}
}
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>col[i];
	}
	memset(h,-1,sizeof h);
	for(int i=1;i<n;i++){
		int a,b;
		cin>>a>>b;
		add(a,b);add(b,a);
	}
	sum_siz=n;
	rt=0;mx[rt]=inf;
	get_root(1,-1);
	get_root(rt,-1);
	solve(rt,-1);
	for(int i=1;i<=n;i++){
		cout<<res[i]<<'\n';
	}
	return 0;
}
posted @   zxh923  阅读(15)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示