树的重心

定义 1:删去该点后最大子树最小的点
定义 2:删去该点后所有子树大小均不超过 n/2 的点

两个定义是等价的。如果一个点有超过 n/2 的子树,那么往这个方向走一步,其最大子树会变小。

性质:

  • 一棵树最多有 2 个重心且相邻
  • 重心到所有点距离和最小
  • 可以用调整法证明(相当于换根),P2986 [USACO10MAR] Great Cow Gathering G 这题奶牛的集会地点相当于在带权重心

例题:P5666 [CSP-S2019] 树的重心

分析:对于 \(40\%\) 的数据,枚举删除的边,求两棵树的重心即可,时间复杂度为 \(O(n^2)\)

对于链的情况,每棵树重心一定是链中点,枚举删除的边后可以 \(O(1)\) 计算重心,总时间复杂度为 \(O(n)\)

对于完美二叉树的情况,重心可以直接分析:

image

参考代码
#include <cstdio>
#include <vector>
using std::vector;
using ll = long long;
const int N = 300005;
const ll INF = 1e18;
vector<int> tree[N];
int sz[N], chain[N], idx, c1, c2;
ll minsum;
bool perfect;
ll dfs(int u, int fa, int d) {
    sz[u] = 1;
    ll res = d;
    for (int v : tree[u]) {
        if (v == fa) continue;
        res += dfs(v, u, d + 1);
        sz[u] += sz[v];
    }
    return res;
}
void calc(int u, int fa, ll sum, int n) {
    if (sum < minsum) {
        minsum = sum; c1 = u; c2 = 0;
    } else if (sum == minsum) {
        c2 = u;
    }
    for (int v : tree[u]) {
        if (v == fa) continue;
        calc(v, u, sum + n - 2 * sz[v], n);
    }
}
bool check_chain(int n) {
    for (int i = 1; i <= n; i++) 
        if (tree[i].size() > 2) return false;
    return true;
}
void dfs_chain(int u, int fa) {
    chain[++idx] = u;
    for (int v : tree[u]) {
        if (v == fa) continue;
        dfs_chain(v, u);
    }
}
int getcenter(int l, int r) {
    int s = l + r;
    if (s % 2 == 0) return chain[s / 2];
    else return chain[s / 2] + chain[s / 2 + 1];
}
int check_perfect_size(int u, int fa, int correct_size) {
    int sz = 1;
    for (int v : tree[u]) {
        if (v == fa) continue;
        sz += check_perfect_size(v, u, correct_size / 2);
    }
    if (sz != correct_size) perfect = false;
    return sz;
}
bool check_perfect(int n) {
    int root = 0;
    for (int i = 1; i <= n; i++) 
        if (tree[i].size() == 2) {
            if (root != 0) return false;
            root = i;
        }
    perfect = true;
    check_perfect_size(root, 0, n);
    return perfect;
}
void solve() {
    int n; scanf("%d", &n);
    for (int i = 1; i <= n; i++) tree[i].clear();
    for (int i = 1; i < n; i++) {
        int u, v; scanf("%d%d", &u, &v);
        tree[u].push_back(v);
        tree[v].push_back(u);
    }

    ll ans = 0;
    if (check_chain(n)) {
        for (int i = 1; i <= n; i++) {
            if (tree[i].size() == 1) {
                idx = 0; dfs_chain(i, 0); break;
            }
        }
        for (int i = 1; i < n; i++) {
            // delete the edge (chain[i], chain[i+1])
            ans += getcenter(1, i);
            ans += getcenter(i + 1, n);
        }
        printf("%lld\n", ans);
    } else if (check_perfect(n)) {
        int root = 1;
        ll ans = 1ll * n * (n + 1) / 2;
        for (int i = 1; i <= n; i++)    
            if (tree[i].size() == 2) {
                root = i; break;
            }
        ans -= root;
        ans += 1ll * (n - 1) / 2 * tree[root][0];
        ans += 1ll * (n - 1) / 2 * tree[root][1];
        ans += 1ll * (n + 1) / 2 * root;
        printf("%lld\n", ans);
    } else {
        for (int u = 1; u <= n; u++) {
            for (int v : tree[u]) {
                // delete the edge (u,v)
                ll sum1 = dfs(u, v, 0), sum2 = dfs(v, u, 0);
                minsum = INF; c1 = u; c2 = 0; calc(u, v, sum1, sz[u]); ans += c1 + c2;
                minsum = INF; c1 = v; c2 = 0; calc(v, u, sum2, sz[v]); ans += c1 + c2;
            }
        }
        printf("%lld\n", ans / 2);
    }
    
}
int main()
{
    int t; scanf("%d", &t);
    for (int i = 1; i <= t; i++) {
        solve();
    }
    return 0;
}

对于一般情况,可以考虑每个点作为重心的贡献。

首先拿出整棵树的一个重心作为根节点 \(root\)

对于一个不为 \(root\) 的点 \(x\),如果它是删边后某棵树的重心,那么删的边肯定不在 \(x\) 的子树里,否则 \(x\) 向父节点方向发展的子树还是会保持超过 \(n/2\)\(x\) 不可能是重心。

设在 \(x\) 子树外割掉的是一个大小为 \(S\) 的部分,设 \(g_x\) 表示 \(x\) 向下的子树中最大的那棵的大小,则 \(x\) 要做删边后的重心必须满足 \(2 \times (n - S - sz_x) \le n - S\) 并且 \(2 \times g_x \le n - S\)

\(n - 2 \times sz_x \le S \le n - 2 \times g_x\),其中 \(sz_x\)\(g_x\) 可以在求初始重心的 DFS 过程中求出。

对于符合条件的 \(S\) 的数量,可以使用树状数组维护,当根从 \(u\) 换到 \(v\) 时,只需将 \(sz_u\) 处减 \(1\),将 \(n - sz_v\) 处加 \(1\),那符合条件的数量就是一个区间求和了。

但这个是包含子树内的贡献的,想要去掉可以再用一个树状数组, 按 DFS 的顺序插入每个 \(sz_u\),那么进入 \(u\) 时和回溯离开时的差值就是子树内的贡献,所以可以在进入时把答案加上这时该查询区间的结果,在回溯离开时减去那时该查询区间的结果,这样就相当于减去了整棵子树内的贡献。

接下来只差 \(root\) 本身的贡献还没计算。

对于 \(root\),如果删的边不再其最大子树中,显然这时 \(root\) 的最大子树还是原来的最大子树,那就需要两倍的这个最大子树大小 \(\le n - S\)

否则最大子树就被破坏了,此时只需要满足原来的次大子树的两倍大小 \(\le n - S\)

所以可以先求出最大子树和次大子树对应节点,进行 DFS,考虑删除每一条边的情况,分两种情况查询结果即可。

这样答案就全部统计完成了。

参考代码
#include <cstdio>
#include <vector>
#include <algorithm>
using std::vector;
using std::max;
using ll = long long;
const int N = 300005;
vector<int> tree[N];
int center, n, sz[N], g[N], max1, max2;
ll ans;
bool flag[N];
struct BIT {
    ll c[N];
    void clear(int n) {
        for (int i = 0; i <= n; i++) c[i] = 0;
    }
    int lowbit(int x) {
        return x & -x;
    }    
    void add(int x, int delta) {
        while (x <= n) {
            c[x] += delta;
            x += lowbit(x);
        }
    }
    ll query(int x) {
        ll res = 0;
        while (x > 0) {
            res += c[x];
            x -= lowbit(x);
        }
        return res;
    }
};
BIT bit1, bit2;

void dfs1(int u, int fa) { // 预处理重心、每棵子树大小、每个点下方最大子树大小
    sz[u] = 1; g[u] = 0;
    for (int v : tree[u]) {
        if (v == fa) continue;
        dfs1(v, u);
        sz[u] += sz[v];
        if (sz[v] > g[u]) g[u] = sz[v];
    }
    if (max(g[u], n - sz[u]) <= n / 2 && center == 0) {
        center = u;
    }
}
void dfs2(int u, int fa) { // 考虑么个点作为重心的贡献
    if (u != center) {
        ans += 1ll * u * (bit1.query(n - 2 * g[u]) - bit1.query(n - 2 * sz[u] - 1));
        // 减去子树下的贡献:先加上此时的查询结果
        ans += 1ll * u * (bit2.query(n - 2 * g[u]) - bit2.query(n - 2 * sz[u] - 1));
    }
    bit2.add(sz[u], 1);
    for (int v : tree[u]) {
        if (v == fa) continue;
        // 换根
        bit1.add(sz[u], -1); bit1.add(n - sz[v], 1);
        if (flag[u]) flag[v] = true; 
        // 根据此时是否在最大子树分两种情况查询结果
        if (2 * sz[flag[v] ? max2 : max1] <= n - sz[v]) ans += center;
        dfs2(v, u);
        bit1.add(sz[u], 1); bit1.add(n - sz[v], -1);
    }
    if (u != center) {
        // 减去子树下的贡献:回溯时减去此时的查询结果
        ans -= 1ll * u * (bit2.query(n - 2 * g[u]) - bit2.query(n - 2 * sz[u] - 1));
    }
}
void solve() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
        tree[i].clear();
    }
    for (int i = 1; i < n; i++) {
        int u, v; scanf("%d%d", &u, &v);
        tree[u].push_back(v); tree[v].push_back(u);
    }
    center = 0;
    dfs1(1, 0);
    // center是整棵树的重心
    dfs1(center, 0);
    bit1.clear(n); bit2.clear(n); 
    for (int i = 1; i <= n; i++) { // 树状数组维护每个可以割的大小S
        bit1.add(sz[i], 1); flag[i] = false;
    }
    ans = 0;
    max1 = max2 = 0; // 根节点的最大、次大子树
    for (int v : tree[center]) {
        if (max1 == 0 || sz[v] > sz[max1]) {
            max2 = max1; max1 = v;
        } else if (max2 == 0 || sz[v] > sz[max2]) {
            max2 = v;
        }
    }
    flag[max1] = true;
    dfs2(center, 0);
    printf("%lld\n", ans);
}
int main()
{
    int t; scanf("%d", &t);
    for (int i = 1; i <= t; i++) solve();
    return 0;
}
posted @ 2024-11-21 21:05  RonChen  阅读(7)  评论(0编辑  收藏  举报