点分治常用于静态树上的路经统计问题,我们可以很自然的设计出这样一种分治算法:
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
") : printf("NAY
");
return 0;
}
其他例题:P4178 Tree , P2634 [国家集训队]聪聪可可