点分治总结

点分治常用于静态树上的路经统计问题,我们可以很自然的设计出这样一种分治算法:
1.找出根结点Root;
2.计算以Root为根的树的答案;
3.删除结点Root,分治解决Root的每个子树;
但这样并不是最优,当树退化成链时,递归层数就会退化为O(N), 整个程序时间复杂度也会退化成O(N ^ 2)。
为了解决这个问题,我们每次找的Root必须是树的重心。这样的话递归的复杂度就能稳定在O(logN)。

树的重心:

树的重心也叫树的质心。找到一个点,其所有的子树中最大的子树节点数最少,那么这个点就是这棵树的重心,删去重心后,生成的多棵树尽可能平衡。 --百度百科

不难发现,每次取树的重心作为Root,其所有子树大小都不大于\(size[Root]/2\),(\(size[Root]\)表示以Root为根的子树的结点数),这样递归下去,复杂度就可以控制在O(lonN)。
如何找重心: 一遍dfs就可以。

void find(int x, int fa) {
	size[x] = 1; maxp[x] = 0;
	//size[x] 以x为根的子树的结点数 maxp[x] 与x相连的所有子树中最大子树的大小
	for(int i = head[x]; i; i = nextt[i]) {
		int y = to[i]; if(y == fa || vis[y]) continue;
		find(y, x);
		maxp[x] = max(maxp[x], size[y]);
		size[x] += size[y];
	}
	maxp[x] = max(maxp[x], sum - size[x]);
	if(maxp[x] < maxp[root]) root = x;
}

如何点分治:

void solve(int x) {
	ans[0] = vis[x] = true; calc(x); //calc() 统计答案
	for(int i = head[x]; i; i = nextt[i]) {
		int y = to[i];
		if(vis[y]) continue;
		sum = size[y];
		root = 0; maxp[root] = inf;
		find(y, y);//对于每个子树找重心,递归
		solve(root);
	}
}

如何统计答案:
以luoguP3806 【模板】点分治1 为例
将所有询问记在一个数组中,一遍点分治直接统计完。
 用一个桶记录一下当前子树到根结点距离为某个数的点是否存在,为避免两个点在同一子树中导致路径重复,我们每次计算完一个子树,再将桶更新。
在其他许多路径计数问题中,这样类似的思想可以避免重复计算,也就免去了容斥这一步,降低了思维难度。

void calc(int x) {
	int p = 0, save[N];//用于清空桶
	for(int i = head[x]; i; i = nextt[i]) {
		int y = to[i]; if(vis[y]) continue;
		dis[y] = d[i]; tot = 0; dfs(y, x); //遍历子树,计算dis[]
		for(int j = tot; j >= 1; j--) {
			for(int q = 1; q <= m; q++) {
				if(qu[q] >= tmp[j]) flag[q] |= ans[qu[q] - tmp[j]];//判断是否存在
			}
		}
		for(int j = 1; j <= tot; j++) {//更新桶
			save[++p] = tmp[j];
			ans[tmp[j]] = true;
		}
	}
	for(int i = 1; i <= p; i++) ans[save[i]] = false;//memset()会Tle
}

完整代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
const int N = 10005;
const int inf = (1 << 31) - 1;
int n, m, cnt = 1, k, root, size[N], maxp[N], qu[N];
int nextt[N << 1], head[N], to[N << 1], d[N << 1];
int sum, tot = 0, tmp[N], dis[N];
bool vis[N], ans[100000005], flag[N];
void add(int x, int y, int z) {
	nextt[++cnt] = head[x]; d[cnt] = z;
	to[cnt] = y; head[x] = cnt;
}
void find(int x, int fa) {
	size[x] = 1; maxp[x] = 0;
	//size[x] 以x为根的子树的结点数 maxp[x] 与x相连的所有子树中最大子树的大小
	for(int i = head[x]; i; i = nextt[i]) {
		int y = to[i]; if(y == fa || vis[y]) continue;
		find(y, x);
		maxp[x] = max(maxp[x], size[y]);
		size[x] += size[y];
	}
	maxp[x] = max(maxp[x], sum - size[x]);
	if(maxp[x] < maxp[root]) root = x;
}
void dfs(int x, int fa) {
	tmp[++tot] = dis[x];
	for(int i = head[x]; i; i = nextt[i]) {
		int y = to[i]; if(vis[y] || y == fa) continue;
		dis[y] = dis[x] + d[i];
		dfs(y, x);
	}
}
void calc(int x) {
	int p = 0, save[N];//用于清空桶
	for(int i = head[x]; i; i = nextt[i]) {
		int y = to[i]; if(vis[y]) continue;
		dis[y] = d[i]; tot = 0; dfs(y, x); //遍历子树,计算dis[]
		for(int j = tot; j >= 1; j--) {
			for(int q = 1; q <= m; q++) {
				if(qu[q] >= tmp[j]) flag[q] |= ans[qu[q] - tmp[j]];//判断是否存在
			}
		}
		for(int j = 1; j <= tot; j++) {//更新桶
			save[++p] = tmp[j];
			ans[tmp[j]] = true;
		}
	}
	for(int i = 1; i <= p; i++) ans[save[i]] = false;//memset()会Tle
}
void solve(int x) {
	ans[0] = vis[x] = true; calc(x); //calc() 统计答案
	for(int i = head[x]; i; i = nextt[i]) {
		int y = to[i];
		if(vis[y]) continue;
		sum = size[y];
		root = 0; maxp[root] = inf;
		find(y, y);
		solve(root);
	}
}
int main() {
//	freopen("data.in", "r", stdin);
	scanf("%d%d", &n, &m);
	for(int i = 1, u, v, w; i <= n - 1; i++) {
		scanf("%d%d%d", &u, &v, &w);
		add(u, v, w); add(v, u, w);
	}
	for(int i = 1; i <= m; i++) {
		scanf("%d", &qu[i]);
	}
	root = 0; sum = n; maxp[root] = n;
	find(1, 1);
	solve(root); 
	for(int i = 1; i <= m; i++) 
		flag[i] == true ? printf("AYE\n") : printf("NAY\n");
	return 0;
}

其他例题:P4178 Tree , P2634 [国家集训队]聪聪可可

posted @ 2020-02-13 19:08  Mcggvc  阅读(122)  评论(0编辑  收藏  举报