Skip to content

点分治学习笔记

检测到 KaTeX 加载失败,可能会导致文中的数学公式无法正常渲染。

点分治是用来解决树上路径问题的一种方法。

树的重心

定义

一棵树的重心是该树以该点为根时最大子树最小的点。

性质

  1. 以树的重心为根时,所有子树的大小都不超过整棵树大小的一半。
  2. 树至多有两个重心。如果树有两个重心,那么它们相邻。此时树一定有偶数个节点,且可以被划分为两个大小相等的分支,每个分支各自包含一个重心。
  3. 树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么到它们的距离和一样。反过来,距离和最小的点一定是重心。
  4. 往树上增加或减少一个叶子,如果原节点数是奇数,那么重心可能增加一个,原重心仍是重心;如果原节点数是偶数,重心可能减少一个,另一个重心仍是重心。
  5. 把两棵树通过一条边相连得到一棵新的树,则新的重心在较大的一棵树一侧的连接点与原重心之间的简单路径上。如果两棵树大小一样,则重心就是两个连接点。

求法

求重心可以用一次 DFS 完成。

在 DFS 中计算每个子树的大小,记录“向下”的子树的最大大小,利用总点数 - 当前子树(这里的子树指有根树的子树)的大小得到“向上”的子树的大小,然后就可以依据定义找到重心了。

代码

int root, siz[N], max[N];

void find(int u, int fa, int tot) {
    siz[u] = 1;
    max[u] = 0;

    for (auto e : g[u]) {
        int v = e.first,
            w = e.second;

        if (v == fa || vis[v]) continue;

        find(v, u, tot);

        siz[u] += siz[v];
        max[u] = std::max(max[u], siz[v]);
    }

    max[u] = std::max(max[u], tot - siz[u]);

    if (max[u] < max[root]) root = u;
}

点分治

实现

先选择一个节点 pp 作为根节点,则相对 pp 而言,树上的路径可以分为两类:

  1. 经过根节点 pp
  2. 包含于 pp 的某一棵子树中。

根据分治的思想,对于第 22 类路径,可以将 pp 的每棵子树作为子问题递归求解。

对于第 11 类路径,可以从根节点 pp 将路径分为 upu \to ppvp \to v 两端。然后从 pp 出大对整棵树进行 DFS,求出 dist\it dist 数组表示从根节点 pp 走到节点 xx 的距离。

对于 P3806 【模板】点分治 1 这道题,在求出某棵子树的 dist\it dist 数组之后,若存在一条长为 queryjdisti\it{query}_j - \it{dist}_i 的路径(这个路径是上一棵及以前的子树中到根节点的),则整棵树中一定存在长为 queryj\it{query}_j 的路径,标记答案数组即可。本轮循环完成后再将本次的 dist\it{dist} 数组标记到 exists\rm{exists} 数组中以供下次使用。

代码

#include <iostream>
#include <limits>
#include <queue>
#include <vector>

using std::cin;
using std::cout;
const char endl = '\n';

const int N = 1e4 + 5,
          K = 1e8 + 5;

int n, m, query[N];
int root, siz[N], max[N];
int cnt, dist[N];
bool vis[N], exists[K], ans[N];
std::vector<std::pair<int, int>> g[N];

void find(int u, int fa, int tot) {
    siz[u] = 1;
    max[u] = 0;

    for (auto e : g[u]) {
        int v = e.first,
            w = e.second;

        if (v == fa || vis[v]) continue;

        find(v, u, tot);

        siz[u] += siz[v];
        max[u] = std::max(max[u], siz[v]);
    }

    max[u] = std::max(max[u], tot - siz[u]);

    if (max[u] < max[root]) root = u;
}

void dis(int u, int fa, int sum) {
    dist[++cnt] = sum;

    for (auto e : g[u]) {
        int v = e.first,
            w = e.second;

        if (v == fa || vis[v]) continue;

        dis(v, u, sum + w);
    }
}

void calc(int u) {
    std::queue<int> q;

    for (auto e : g[u]) {
        int v = e.first,
            w = e.second;

        if (vis[v]) continue;

        cnt = 0;
        dis(v, u, w);

        for (int i = 1; i <= cnt; i++) {
            for (int j = 1; j <= m; j++) {
                if (query[j] >= dist[i]) {
                    ans[j] |= exists[query[j] - dist[i]];
                }
            }
        }

        for (int i = 1; i <= cnt; i++) {
            q.push(dist[i]);
            exists[dist[i]] = true;
        }
    }

    while (!q.empty()) {
        exists[q.front()] = false;
        q.pop();
    }
}

void solve(int u) {
    vis[u] = true;
    exists[0] = true;
    calc(u);

    for (auto e : g[u]) {
        int v = e.first,
            w = e.second;

        if (vis[v]) continue;

        max[root = 0] = std::numeric_limits<int>::max();
        find(v, 0, siz[v]);
        solve(root);
    }
}

int main() {
    std::ios::sync_with_stdio(false);

    cin >> n >> m;

    for (int i = 1, u, v, w; i < n; i++) {
        cin >> u >> v >> w;

        g[u].push_back(std::make_pair(v, w));
        g[v].push_back(std::make_pair(u, w));
    }

    for (int i = 1; i <= m; i++) {
        cin >> query[i];
    }

    cnt = 0;
    max[root = 0] = n;
    find(1, 0, n);
    solve(root);

    for (int i = 1; i <= m; i++) {
        cout << (ans[i] ? "AYE" : "NAY") << endl;
    }

    return 0;
}

参考资料

  1. 0x45 点分治,《算法竞赛进阶指南》(ISBN 978-7-83009-313-6,河南电子音像出版社),李煜东,2019 年 5 月第 5 次修订版。
  2. 一种基于错误的寻找重心方法的点分治的复杂度分析,刘承奥,2017 年 9 月 24 日。
  3. 树分治,OI Wiki,2022 年 2 月 13 日。
  4. 点分治学习笔记,黄浩睿,2016 年 6 月 17 日。
  5. 题解:P3806 【模板】点分治 1,niiick,2020 年 8 月 8 日。
  6. 2.14 点分治和点分树,AcWing 算法进阶课,闫学灿,2020 年 11 月 14 日。