樹的重心

dengchengyu發表於2024-11-26

定義 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;
}

相關文章