点分治、点分树速通(树上分治)

题外话:在写这篇文章的时候正值五一放假前夕,部分同学直接请了两天假凑成了七天五一长假,徐某羡慕不已,以至于学习淀粉质的时候有一种大脑被抽离的感觉,就算带着两个耳机,知识还是莫名地飞走,等五一放完,我再把博客们完善完善。。。

前置芝士:分治、树的重心

分治的思想相比大家都很熟悉了,比如归并排序,利用的就是分治思想。而将分治思想运用到树上去,就是我们今天要介绍的淀粉质点分治的内容。来看一条题目:(洛谷P4187)

 题目非常的简单明了,一棵树,点之间的距离小于k,就这么简单。如果用纯暴力枚举的话,枚举每个点u,再枚举另一个点v,求距离,复杂度为O(n^2logn),点的数量为4e4,很明显超了,考虑优化。可以先找到树的重心,如图

 与重心相连的有若干子树,对于任意两个满足题意的点对,我们可以把它分成三种情况:

1、两个点在同一子树内

2、两个点在不同子树内

3、其中一个点时重心 

就像这样

第一种情况,我们相当于大问题的一个子问题,可以把子树继续当成一棵新图然后递归求解即可。

第三种情况也很容易,其中一个是重心,那另一个直接递归遍历子树中的每一个节点看距离是否满足即可

重点在于第二种情况,在这里我们可以利用一个容斥原理的思想,假设第二种情况的所有点对集为S,全集为U,那么S = U - 不满足2的情况,也就是全集U减去每一棵子树的第1、3种情况即可。

那么这么一来,我们就可以用分治的思想把整个问题划分成若干子树的子问题。每个问题求子集合里满足小于等于k的点对,那么我们可以求出每个点对点距离,然后排序,用二分即可。最后的时间复杂度应该是求每个点对的距离O(n),排序和二分O(logn),最后每个划分最多会划分成logn个子问题,所以总共要求O(logn)次,时间复杂度应该是O(nlog^2n)

来分析一下每个代码:

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、求树的重心 

这里求的重心其实是不严谨的重心,因为只要满足任意一棵子树都不超过原树一半的大小,最终的复杂度就一定可以满足O(logn),所以我们只要求一个大致的重心分治点即可。这里仍用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;
}

如果这篇文章对你有帮助,求点赞,求收藏,最重要的是点一个大大的关注,这是对我最大的鼓励,我们下期再见!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值