poj-1741 (点分治模板)

题目

Description

Give a tree with n vertices,each edge has a length(positive integer less than 1001).
Define dist(u,v)=The min distance between node u and v.
Give an integer k,for every pair (u,v) of vertices is called valid if and only if dist(u,v) not exceed k.
Write a program that will count how many pairs which are valid for a given tree.

Input

The input contains several test cases. The first line of each test case contains two integers n, k. (n<=10000) The following n-1 lines each contains three integers u,v,l, which means there is an edge between node u and v of length l.
The last test case is followed by two zeros.

Output

For each test case output the answer on a single line.

Sample Input

5 4
1 2 3
1 3 1
1 4 2
3 5 1
0 0

Sample Output

8

分析

  • 题目大意就是给一棵树,每条边有一个长度,两个点之间的距离就是连接它俩的边的长度之和。求出这棵树中满足距离不大于 K 的点对个数。

  • 这基本上就是树的点分治模板题,我也是第一次打树的分治,主要有以下几个步骤:

  • 对于一棵子树的分治,首先求出它的重心, 我们目的是对这棵子树讨论所有经过重心的路径。(重心的作用是保证时间复杂度)

  • 之后,以此重心为根节点,对这棵树进行一遍深搜,得出每个节点到重心的距离dis[]

  • 之后,这棵子树中相连的路径会经过重心且对答案有贡献的点对(i,j) (i<j)就会是这样: dis[i]+dis[j] <= K 且在去除重心后,i 与 j 不在同一个联通块里

  • 不过显然要满足“不在同一个联通块里”这个条件有点突兀,于是就有了一个小技巧:

  • 先不管在不在一个联通块这个条件,算出当前这棵树的符合路径数,之后再将得出的个数减去 以重心的儿子节点为根的子树内 的 点对 路径距离(经过重心)小于等于K的个数,就行了。

  • (可能这句话有点绕,不会的话看看程序再理解理解,应该也是能领悟到的)

  • 之后,再分治一下现在这棵子树,步骤同上。

  • 一棵子树的重心其实就是你要找到一个点,使得删掉这个点后,这棵子树剩下最大(节点个数最多)联通块最小。

  • 为什么每次都要算一个重心呢?你想想,要是那一棵子树刚好是一条链(就是一整条下来),而默认的根节点又在链的端点上,那么时间不就退化到了还不如暴力的程度了?于是我们求个重心,把时间复杂度保证在了 log 级别里面,就很多了。

程序

#include <cstdio>
#include <algorithm>
#define Max 20010
#define add(u,v,w) (To[++num]=Head[u],Head[u]=num,V[num]=v,W[num]=w)
#define For(x) for (int h=Head[x],o=V[h]; h; h=To[h],o=V[h])
#define Input for (int i=1,u,v,w; i<N; i++) scanf("%d%d%d",&u,&v,&w),add(u,v,w),add(v,u,w)
using namespace std;
int N,K,root,num,ans,cnt,mins,Head[Max],To[Max],V[Max],W[Max],siz[Max],dis[Max],f[Max];
/*
	程序中部分变量说明
	dis[i]	所有路径到 重心 的长度
	siz[i]	以 i 为根节点的子树的大小(当前重心下)
	f[i]	i 是否还存在(分治完一次后就把重心删掉了)
	cnt		记录 dis 的个数(即路径个数)
	root	当前子树的重心
	maxs	当前讨论的点所有子树大小中最大值(并不是全局变量,是尝试每个重心时重新开的一个变量)
	mins	讨论过的点的子树大小中最大的最小值(是全局变量,用来确定哪个才是重心)
*/

int get_size(int x,int fa){		//返回以 x 为根的子树大小,其中 x 父节点为 fa 
	siz[x]=1;
	For(x) if (o!=fa && !f[o])
		siz[x]+=get_size(o,x);
	return siz[x];
}

void get_dis(int x,int d,int fa){	//x 到重心的长度为 d,之后继续 dfs 
	dis[++cnt]=d;
	For(x) if (o!=fa && !f[o])
		get_dis(o,d+W[h],x);
	return;
}

void dfs_root(int x,int tot,int fa) {
	//求目标子树的重心(要求除去 x 点时,它的 maxs 值最小,那么 x 就是这棵子树的重心了),其中 tot 是这棵子树的总大小(节点个数) 
	int maxs=tot-siz[x];	//这棵子树中x 父亲那一支先赋给 maxs 
	For(x) if (o!=fa && !f[o]){
		maxs=max(maxs,siz[o]);
		dfs_root(o,tot,x);
	}
	if (maxs<mins){
		mins=maxs;
		root=x;
	}
	return;
}

int work(int x,int d) {
	//返回以 x 为根的子树内长度小于等于 K 的路径数(两个端点都在子树内) 
	//其实 d 在这里用处只有一个,是在做减法时方便把重心的儿子节点的 dis 先弄好,你也可以在分治的时候弄,不过就稍微有点麻烦了 
	cnt=0;
	get_dis(x,d,0);
	sort(dis+1,dis+cnt+1);
	int daan=0,i=1,j=cnt; 
	while (i<j){
		while (i<j && dis[i]+dis[j]>K) j--;
		daan+=j-i;	//相当于选一条路径 i,另一条可以为 [i+1,j] 里任意一条路径,这样得到的两个点之间长度(经过重心的那条路径)肯定是小于等于 K 的 
		i++;
	}
	return daan;
}

void dfs(int x){	//以 x 为重心分治一下 
	cnt=0;
	mins=Max;
	get_size(x,0);
	dfs_root(x,siz[x],0);
	ans+=work(root,0);
	f[root]=1;
	For(root) if (!f[o]){		//注意这里是以重心开始 
		ans-=work(o,W[h]);		//注意,这里 dis[o] 要先赋成 W[h](即它到重心的距离) 
		dfs(o);
	}
	return;
}

int main(){
	while(scanf("%d%d",&N,&K)!=EOF && N && K){
		Input;
		dfs(1);
		printf("%d
",ans);
				
		num=ans=0;
		for (int i=1; i<=N; i++) Head[i]=f[i]=dis[i]=0;
	}
	return 0;
}

时间复杂度

  • 我们注意到,对于一棵大小为 n 的树,重心选好后,它的分支大小都是不大于 $frac{n}{2} $ 的,那么每次分治后,联通块的大小会至少减少一半,因此最多递归分治 $log_2 n $ 层,每次分治都会把整棵树的点都讨论一次,于是总的时间复杂度就是:

[nlog_2n ]

提示

  • 开始我写的时候sort的边界弄错了,结果一直WA,要注意一下这些细节问题。
原文地址:https://www.cnblogs.com/hehepig/p/6685546.html