树的重心
定义 1:删去该点后最大子树最小的点
定义 2:删去该点后所有子树大小均不超过 n/2 的点
两个定义是等价的。如果一个点有超过 n/2 的子树,那么往这个方向走一步,其最大子树会变小。
性质:
- 一棵树最多有 2 个重心且相邻
- 重心到所有点距离和最小
- 可以用调整法证明(相当于换根),P2986 [USACO10MAR] Great Cow Gathering G 这题奶牛的集会地点相当于在带权重心
例题:P5666 [CSP-S2019] 树的重心
分析:对于
对于链的情况,每棵树重心一定是链中点,枚举删除的边后可以
对于完美二叉树的情况,重心可以直接分析:
参考代码
#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; }
对于一般情况,可以考虑每个点作为重心的贡献。
首先拿出整棵树的一个重心作为根节点
对于一个不为
设在
即
对于符合条件的
但这个是包含子树内的贡献的,想要去掉可以再用一个树状数组, 按 DFS 的顺序插入每个
接下来只差
对于
否则最大子树就被破坏了,此时只需要满足原来的次大子树的两倍大小
所以可以先求出最大子树和次大子树对应节点,进行 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; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?