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