定義 1:刪去該點後最大子樹最小的點
定義 2:刪去該點後所有子樹大小均不超過 n/2 的點
兩個定義是等價的。如果一個點有超過 n/2 的子樹,那麼往這個方向走一步,其最大子樹會變小。
性質:
- 一棵樹最多有 2 個重心且相鄰
- 重心到所有點距離和最小
- 可以用調整法證明(相當於換根),P2986 [USACO10MAR] Great Cow Gathering G 這題奶牛的集會地點相當於在帶權重心
例題:P5666 [CSP-S2019] 樹的重心
分析:對於 \(40\%\) 的資料,列舉刪除的邊,求兩棵樹的重心即可,時間複雜度為 \(O(n^2)\)。
對於鏈的情況,每棵樹重心一定是鏈中點,列舉刪除的邊後可以 \(O(1)\) 計算重心,總時間複雜度為 \(O(n)\)。
對於完美二叉樹的情況,重心可以直接分析:
參考程式碼
#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;
}