POJ 1741 点分治

题意:

给定n个点的树, K值

下面n-1条边

问 两点之间距离<= K的点对有多少

 

采用点分治,无根树转有根树时 根为树的重心(可以把树高度降低,防止树退化成链)

思路:

对于一棵 以u为根的树

以下我们成(a,b)为合法点对(即dist(a,b) <=K)

(a, b) 之间路径是唯一确定的。

 

将点对分2类:

1、两点间路径经过 u 点

2、两点间路径不经过u点 = 两点都在u的同一子树下 (也就是 a,b 有个公共祖先 x ,x是u的儿子节点 )

显然合法点对数 = 第一类+第二类

 

对于第一类://计算树的重心复杂度为 O(n) ,排序为O(nlogn)

经过u点 点对数 = 所有 dist(a,b) <=K 数 - 求和(u 的儿子节点v )(v子树中的合法节点数)

对于所有的 合法节点数 ,我们可以用单调性求出(具体实现在函数 getans() 中)

公式在work函数中实现。

这样计算完后,u节点就直接删去(用vis数组记录)

这样会得到u的儿子形成的森林

 

对于第二类://递归实现,由于用树的重心优化高度,递归次数为 log n

ans += u的所有儿子作为第一类点时的答案

 

总的复杂度为 O(n*logn * logn)

注意一点: 计算树的重心时,树的总节点数会改变(用size表示当前的节点数)

#pragma comment(linker, "/STACK:10240000000000,10240000000000")
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<math.h>
using namespace std;
inline int Max(int a,int b){return a>b?a:b;}
inline int Min(int a,int b){return a<b?a:b;}
#define N 10100

struct Edge{int to, nex, dis;}edge[N<<1];
int head[N], edgenum;
void addedge(int u, int v, int dis){Edge E = {v, head[u], dis};edge[ edgenum ] = E;head[u] = edgenum++;}

int n, K, Ans;
int num[N];//num[i]表示 以i为根的树 节点数
int dp[N];//树重心的定义:dp[i]表示 将i点删去后 最大联通块的点数

int Stack[N], top1, root;
bool vis[N];//vis[i]表示i点是否删去
int size; //** 表示当前 计算的树的节点数 

void getroot(int u, int fa){//找树的重心
	dp[u] = 0; num[u] = 1; //以u为根的子树节点数	
	for(int i = head[u]; ~i; i = edge[i].nex){
		int v = edge[i].to;		
		if(v != fa && !vis[v]){
			getroot(v, u);
			num[u] += num[v];
			dp[u] = Max(num[v], dp[u]);
		}
	}
	dp[u] = Max(dp[u], size - num[u]);
	if(dp[u] < dp[root]) root = u;
}

int dis[N];
inline bool cmp(int i, int j){return dis[i] < dis[j];}

void Find_dis(int u, int fa, int d){ //把该树所有点入栈 并算出每个点到根节点的距离
	Stack[++top1] = u;
	dis[u] = d;
	for(int i = head[u]; ~i; i =edge[i].nex){
		int v = edge[i].to;		if(vis[v] || v == fa)continue;
		Find_dis(v, u, d+edge[i].dis);
	}
}

int getans(int l, int r){//计算树中 距离<=k 的点对数量
	int j = r, ans = 0;
	for(int i = l; i <= r; i++){
		while(dis[ Stack[i] ] + dis[ Stack[j] ] > K && j>i)j--;
		if(i == j)return ans;
		ans += j - i;
	}
	return ans;
}
//先找到重心 (这样转成有根树不会造成树退化的情况) 对于此树计算后,删去根节点(vis数组记录, 因为已经求出所有经过根节点的合法点对,则根节点无用了)
//若合法点对存在于子树中 则要递归计算子树的点对(用f数组判断点对是否在一个子树中) 将子树的重心来转为有根树进行操作
void work(int u, int fa){//计算出 路径经过u点的合法点对
	//路径经过u点的合法点对数 = 总点对数 - 不经过u点(即点对都在一棵子树上)
	dp[0] = N; size = num[u];//注意初始化
	root = 0;

	getroot(u, fa);//root 为u的联通块中的重心

	top1 = 0;  int top2 = 0;
	for(int i = head[root]; ~i; i =edge[i].nex){
		int v = edge[i].to;		if(vis[v]) continue;

		top2 = top1;			//Stack[top2+1] 到 Stack[top1] 之间的点就是v子树所有的点
		Find_dis(v, root, edge[i].dis);
		sort(Stack + top2 +1, Stack + top1 +1 , cmp);
		Ans -= getans(top2+1, top1);
	}
	//Stack[1] - Stack[top1]就是root子树所有的点
	Stack[ ++top1 ] = root; 	dis[root] = 0;
	sort(Stack + 1, Stack + top1 +1, cmp);
	Ans += getans(1, top1);

	vis[root] = 1;//去掉root点

	for(int i = head[root]; ~i; i = edge[i].nex)//路径不经过u点的点对数
		if(!vis[ edge[i].to]) work(edge[i].to, root);
}
void init(){
	memset(head, -1, sizeof(head)); edgenum = 0;
	memset(vis, 0, sizeof(vis));	Ans = 0;
}
int main(){
	while(scanf("%d %d", &n, &K), n+K){
		init();
		for(int i = 1; i < n; i++)
		{
			int u, v, d; scanf("%d %d %d", &u, &v, &d);
			addedge(u, v, d);	addedge(v, u, d);
		}
		num[1] = n;
		work(1, 0);
		printf("%d\n",Ans);
	}
	return 0;
}


 

 

你可能感兴趣的:(POJ 1741 点分治)