点分治模板 and Tree POJ-1741 题解

点分治模板 and Tree POJ-1741 题解

感觉这道题挺适合点分治入门的。

Part 0

点分治可以解决很多询问路径长度之类的问题。

一般的路径长度问题(就如本题),假如数据范围小,就可以先预处理出每个点到根的距离,再枚举路径的两端点,求出LCA,就可以算出每个点对的距离。

但这要 O ( n 2 log ( n ) ) O(n^2 \cdot \log(n)) 的复杂度啊,还是太大了,于是,就有了点分治。

Part 1

如果对于一棵树,我们确定了一个根,那么对于任意一条路径,它只有两种可能,要么经过根,要么不经过根,也就是在根节点的某个子节点的子树内,故我们处理完经过根的路径,再把根从这颗树上删掉(也就是把一棵树分成了好多颗树)。然后再处理其他路径。其他路径就不会经过被删去的点,这样复杂度就会降下来。

如何处理跟放在后面再讲,因为与点分治模板无关。

首先,如何确定点分治的每一个根是有套路的,如果乱确定复杂度是很大的,我们要使断开后每个子树尽量平均(也就是断开后的最大的子树的大小最小)。于是便有了如下确定根的方法。

void Get_Root(int now,int fa){//sz 子树大小 mx_sz 子节点对应的最大子树大小 mark 这个点是否已被删除 
	sz[now]=1,mx_sz[now]=0;
	for(int i=head[now];i;i=edge[i].nx){
		int nxt=edge[i].to;
		if(mark[nxt]||nxt==fa)continue;
		Get_Root(nxt,now);
		sz[now]+=sz[nxt];
		check_max(mx_sz[now],sz[nxt]);
	}
	check_max(mx_sz[now],tot_sz-sz[now]);//要注意还要处理上面的子树。 
	if(mx_sz[now]<mx_sz[Root]||Root==0)Root=now;
}
//调用就是这样
Root=0,tot_sz=n;
Get_Root(1,0);
dfs(Root);

其实这也是点分治的精髓,也是解决这类问题复杂度的保证。

Part 2

以下就是解决本题的方法(其中也包含点分治模板)

处理经过根的路径可以用DP来处理。因为一条路径由根可以分成两条路径,或是一条路径(另一条就是长度为零的路径)。我们可以枚举一条路径,从而得出另一条路径的长度区间,从而计数,当然,我们要先把所有一端为根的路径预处理出来。如下:

void Get_dis(int now,int fa,int res){
	for(int i=head[now];i;i=edge[i].nx){
		int nxt=edge[i].to;
		if(nxt==fa||mark[nxt])continue;
		dis[++n_dis]=res+edge[i].d;
		Get_dis(nxt,now,dis[n_dis]);
	}
}

但是还有一种情况没有考虑到,那就是如果两条路径属于同一个根的子节点的子树,也就是他们的关系如下。

这样可以先处理一遍之后再在此颗子树里单独处理一遍这种路径(就是把这个子树里的链全都抽出来计算一遍答案并减掉,注意要加上其与当前枚举的根之间的边权),就可以删去这种不合法的状态,然后就可以快乐的计数了。

int Get_ans(int now,int add){
	dis[n_dis=1]=add;//若是初次计数,这个add是用来表示那种另一个长度为0的路径的。
	//但若是为了消去不合法情况的,这个add就是用来算上那个子节点到当前枚举这个根的距离的 
	Get_dis(now,0,add);//注意初始为add 
	sort(dis+1,dis+n_dis+1);//把所有距离排序
	int L=1,res=0;
	while(L<n_dis&&K-dis[L]>=dis[L]){
		int R=Find_R(L+1,K-dis[L]);//找到最长的可以匹配的路径
		if(L<=R)res+=R-L;
		L++;
	}
	return res;
}
void Get_Root(int now,int fa){//sz 子树大小 mx_sz 子节点对应的最大子树大小 mark 这个点是否已被删除 
	sz[now]=1,mx_sz[now]=0;
	for(int i=head[now];i;i=edge[i].nx){
		int nxt=edge[i].to;
		if(mark[nxt]||nxt==fa)continue;
		Get_Root(nxt,now);
		sz[now]+=sz[nxt];
		check_max(mx_sz[now],sz[nxt]);
	}
	check_max(mx_sz[now],tot_sz-sz[now]);//要注意还要处理上面的子树。 
	if(mx_sz[now]<mx_sz[Root]||Root==0)Root=now;
}
void dfs(int now){
	mark[now]=1,ans+=Get_ans(now,0);
	for(int i=head[now];i;i=edge[i].nx){
		int nxt=edge[i].to;
		if(mark[nxt])continue;//如果这个点已经删去
		ans-=Get_ans(nxt,edge[i].d);//删去不合法计数,记住要算上它到当前枚举的根的距离
		Root=0,tot_sz=sz[nxt];
		Get_Root(nxt,now),dfs(Root);
	}
}

整个代码就是酱紫

#include<cstdio>
#include<cstring>
#include<algorithm>
#define M 10005
using namespace std;
void check_max(int &x,int y){if(x<y)x=y;}
struct E{
	int to,nx,d;
}edge[M<<1];
int tot,head[M]; 
void Addedge(int a,int b,int d){
	edge[++tot].to=b;
	edge[tot].nx=head[a];
	edge[tot].d=d;
	head[a]=tot;
}
bool mark[M];//这个点是否删去
int sz[M],mx_sz[M],tot_sz;//在下面的函数有解释
int Root,ans;
int dis[M],n_dis,K;
void Get_dis(int now,int fa,int res){
	for(int i=head[now];i;i=edge[i].nx){
		int nxt=edge[i].to;
		if(nxt==fa||mark[nxt])continue;
		dis[++n_dis]=res+edge[i].d;
		Get_dis(nxt,now,dis[n_dis]);
	}
}
int Find_R(int L,int x){//二分找最右边的满足与当前枚举的链加起来<=K的位置
	int R=n_dis,res=0;
	while(L<=R){
		int mid=(L+R)>>1;
		if(dis[mid]<=x){
			L=mid+1;
			res=mid;
		}else R=mid-1;
	}
	return res;
}
int Get_ans(int now,int add){
	dis[n_dis=1]=add;//若是初次计数,这个add是用来表示那种另一个长度为0的路径的。
	//但若是为了消去不合法情况的,这个add就是用来算上那个子节点到当前枚举这个根的距离的 
	Get_dis(now,0,add);//注意初始为add 
	sort(dis+1,dis+n_dis+1);//把所有距离排序
	int L=1,res=0;
	while(L<n_dis&&K-dis[L]>=dis[L]){
		int R=Find_R(L+1,K-dis[L]);//找到最长的可以匹配的路径
		if(L<=R)res+=R-L;
		L++;
	}
	return res;
}
void Get_Root(int now,int fa){//sz 子树大小 mx_sz 子节点对应的最大子树大小 mark 这个点是否已被删除 
	sz[now]=1,mx_sz[now]=0;
	for(int i=head[now];i;i=edge[i].nx){
		int nxt=edge[i].to;
		if(mark[nxt]||nxt==fa)continue;
		Get_Root(nxt,now);
		sz[now]+=sz[nxt];
		check_max(mx_sz[now],sz[nxt]);
	}
	check_max(mx_sz[now],tot_sz-sz[now]);//要注意还要处理上面的子树。 
	if(mx_sz[now]<mx_sz[Root]||Root==0)Root=now;
}
void dfs(int now){
	mark[now]=1,ans+=Get_ans(now,0);
	for(int i=head[now];i;i=edge[i].nx){
		int nxt=edge[i].to;
		if(mark[nxt])continue;//如果这个点已经删去
		ans-=Get_ans(nxt,edge[i].d);//删去不合法计数,记住要算上它到当前枚举的根的距离
		Root=0,tot_sz=sz[nxt];
		Get_Root(nxt,now),dfs(Root);
	}
}
int main(){
	int n;
	while(scanf("%d%d",&n,&K)){
		if(!n&&!K)return 0;
		tot=0;
		ans=0;
		memset(head,0,sizeof(head));
		memset(mark,0,sizeof(mark));//注意每次要清空哦 
		for(int i=1;i<n;i++){
			int a,b,d;
			scanf("%d%d%d",&a,&b,&d);
			Addedge(a,b,d);
			Addedge(b,a,d);
		}
		Root=0,tot_sz=n;
		Get_Root(1,0);
		dfs(Root);
		printf("%d\n",ans);
	}
	return 0;
}

当然,这个模板不能适应所有点分治的题目,但大体上还是一样的,每个题目所求不同,要随机应变。

猜你喜欢

转载自blog.csdn.net/qq_35320178/article/details/86930897
今日推荐