题外话:在写这篇文章的时候正值五一放假前夕,部分同学直接请了两天假凑成了七天五一长假,徐某羡慕不已,以至于学习淀粉质的时候有一种大脑被抽离的感觉,就算带着两个耳机,知识还是莫名地飞走,等五一放完,我再把博客们完善完善。。。
前置芝士:分治、树的重心
分治的思想相比大家都很熟悉了,比如归并排序,利用的就是分治思想。而将分治思想运用到树上去,就是我们今天要介绍的淀粉质点分治的内容。来看一条题目:(洛谷P4187)
题目非常的简单明了,一棵树,点之间的距离小于k,就这么简单。如果用纯暴力枚举的话,枚举每个点u,再枚举另一个点v,求距离,复杂度为,点的数量为4e4,很明显超了,考虑优化。可以先找到树的重心,如图
与重心相连的有若干子树,对于任意两个满足题意的点对,我们可以把它分成三种情况:
1、两个点在同一子树内
2、两个点在不同子树内
3、其中一个点时重心
就像这样
第一种情况,我们相当于大问题的一个子问题,可以把子树继续当成一棵新图然后递归求解即可。
第三种情况也很容易,其中一个是重心,那另一个直接递归遍历子树中的每一个节点看距离是否满足即可
重点在于第二种情况,在这里我们可以利用一个容斥原理的思想,假设第二种情况的所有点对集为S,全集为U,那么S = U - 不满足2的情况,也就是全集U减去每一棵子树的第1、3种情况即可。
那么这么一来,我们就可以用分治的思想把整个问题划分成若干子树的子问题。每个问题求子集合里满足小于等于k的点对,那么我们可以求出每个点对点距离,然后排序,用二分即可。最后的时间复杂度应该是求每个点对的距离,排序和二分
,最后每个划分最多会划分成logn个子问题,所以总共要求
次,时间复杂度应该是
。
来分析一下每个代码:
1、求子树
递归遍历,没什么好说的
int get_size(int u, int fa) { // 求子树大小
if (st[u]) return 0;
int res = 1;
for (int i = h[u]; ~i; i = ne[i])
if (e[i] != fa)
res += get_size(e[i], u);
return res;
}
2、求树的重心
这里求的重心其实是不严谨的重心,因为只要满足任意一棵子树都不超过原树一半的大小,最终的复杂度就一定可以满足,所以我们只要求一个大致的重心分治点即可。这里仍用dfs搜索,每次判断删掉这个点后,其子树的最大值和父节点所在连通块是否均不大于原树的一半
// 求重心(保存在wc里),返回值为当前子树的大小
int get_wc(int u, int fa, int tot, int& wc) { // tot为当前原树点的总数
if (st[u]) return 0; // 已经搜过的点直接返回
int sum = 1, ms = 0; // sum为所有子树大小,ms为最大子树大小
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j == fa) continue;
int t = get_wc(j, u, tot, wc);
ms = max(ms, t);
sum += t;
}
ms = max(ms, tot - sum); // 看当前子树和父节点所在连通块的最大值
if (ms <= tot / 2) wc = u; // 是否不大于原树点的总数的一半
return sum;
}
3、求两点之间的距离
求的是当前子树里所有点到重心(根节点)的距离(包括在同一棵子树里的点,方便后续容斥筛除),存储在数组q内
void get_dist(int u, int fa, int dist, int& qt) {
if (st[u]) return;
q[qt ++ ] = dist; // p数组存所有子树的距离,q存当前子树的距离
for (int i = h[u]; ~i; i = ne[i])
if (e[i] != fa)
get_dist(e[i], u, dist + w[i], qt); // 递归搜索邻边
}
4、求一棵子树上满足题意的点对(双指针)
将子树的每一个点到重心的距离排序后,可以发现,对于每一个点i,都会有一个最大的j是的dist[i]+dist[j] <= k;而随着i的减小,j一定是单调递增的,所以我们可以利用双指针算法来求出所有满足情况的点对
int get(int a[], int k) { // 双指针求一棵子树中满足条件的点对
sort(a, a + k);
int res = 0;
for (int i = k - 1, j = -1; i >= 0; i -- )
{
while (j + 1 < i && a[j + 1] + a[i] <= m) j ++ ;
j = min(j, i - 1);
res += j + 1;
}
return res;
}
5、核心函数calc
int calc(int u)
{
if (st[u]) return 0;
int res = 0;
get_wc(u, -1, get_size(u, -1), u); // 将u设置为当前子树的重心
st[u] = true; // 删除重心
// p数组存所有子树的距离,q存当前子树的距离
int pt = 0;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i], qt = 0;
get_dist(j, -1, w[i], qt);
res -= get(q, qt); // 容斥原理减去所有子树中满足条件的点对
for (int k = 0; k < qt; k ++ ) {
if (q[k] <= m) res ++ ; // 加上一个点是重心的情况
p[pt ++ ] = q[k];
}
}
res += get(p, pt);
for (int i = h[u]; ~i; i = ne[i]) res += calc(e[i]);
return res;
}
最后附上主函数代码
int main() {
while (scanf("%d%d", &n, &m), n || m) {
memset(st, 0, sizeof st);
memset(h, -1, sizeof h);
idx = 0;
for (int i = 0; i < n - 1; i ++ ) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
printf("%d\n", calc(0));
}
return 0;
}
如果这篇文章对你有帮助,求点赞,求收藏,最重要的是点一个大大的关注,这是对我最大的鼓励,我们下期再见!