换根维护最大相邻组件,并沿最大子树链快速定位每个分割树的重心。
OJ: luogu
题目 ID: P5666
难度:省选/提高
标签:重心换根倍增python
日期: 2026-07-17 02:00
题意
删除原树每条边,分别求两个连通块的重心编号和,最后求总和。
思路
一个连通块的重心沿“最大相邻组件”方向向下走,直到下一步子树不足半数。预处理每个节点的最大子树链及其倍增跳表;换根遍历一条边时临时交换两侧的父子关系和子树大小,分别定位两侧重心后恢复现场。
Python 知识
array("i")保存多层跳表和子树大小。- 用显式栈保存待恢复状态,模拟递归换根,避免 30 万深度的 Python 调用栈。
rebuild只更新发生换根的节点,减少跳表维护开销。
代码
python
import sys
from array import array
input = sys.stdin.buffer.readline
test_cases = int(input())
answers = []
for _ in range(test_cases):
n = int(input())
graph = [[] for _ in range(n + 1)]
for __ in range(n - 1):
u, v = map(int, input().split())
graph[u].append(v)
graph[v].append(u)
parent = array("i", [0]) * (n + 1)
order = array("i", [1])
for node in order:
for neighbor in graph[node]:
if neighbor != parent[node]:
parent[neighbor] = node
order.append(neighbor)
subtree = array("i", [1]) * (n + 1)
subtree[0] = 0
heavy = array("i", [0]) * (n + 1)
for node in reversed(order[1:]):
subtree[parent[node]] += subtree[node]
if subtree[node] > subtree[heavy[parent[node]]]:
heavy[parent[node]] = node
log = n.bit_length()
jump = [array("i", [0]) * (n + 1) for _ in range(log)]
for node in range(1, n + 1):
jump[0][node] = heavy[node]
for level in range(1, log):
previous, current = jump[level - 1], jump[level]
for node in range(1, n + 1):
current[node] = previous[previous[node]]
def rebuild(node):
for level in range(1, log):
jump[level][node] = jump[level - 1][jump[level - 1][node]]
def centroid_sum(root):
total = subtree[root]
node = root
for level in range(log - 1, -1, -1):
candidate = jump[level][node]
if candidate and subtree[candidate] * 2 >= total:
node = candidate
value = node
if subtree[node] * 2 == total:
value += parent[node]
return value
answer = 0
# A frame stores the next adjacency position and the two largest current
# neighbour components. Mutations are rolled back after returning.
def largest_two(node):
first = second = 0
for neighbor in graph[node]:
if subtree[neighbor] >= subtree[first]:
second, first = first, neighbor
elif subtree[neighbor] > subtree[second]:
second = neighbor
return first, second
first, second = largest_two(1)
stack = [[1, 0, 0, first, second, None]]
while stack:
frame = stack[-1]
node, father, index, first, second, restore = frame
if index == len(graph[node]):
stack.pop()
if restore is not None:
(old_parent_node, old_parent_child, old_node_size,
old_child_size, old_jump, changed_node, changed_child) = restore
parent[changed_node] = old_parent_node
parent[changed_child] = old_parent_child
subtree[changed_node] = old_node_size
subtree[changed_child] = old_child_size
jump[0][changed_node] = old_jump
rebuild(changed_node)
continue
neighbor = graph[node][index]
frame[2] += 1
if neighbor == father:
continue
answer += centroid_sum(neighbor)
old_state = (parent[node], parent[neighbor], subtree[node],
subtree[neighbor], jump[0][node], node, neighbor)
alternate = second if neighbor == first else first
jump[0][node] = alternate
rebuild(node)
subtree[node] -= subtree[neighbor]
subtree[neighbor] += subtree[node]
parent[node] = neighbor
parent[neighbor] = father
answer += centroid_sum(node)
child_first, child_second = largest_two(neighbor)
stack.append([neighbor, node, 0, child_first, child_second, old_state])
answers.append(str(answer))
print("\n".join(answers))原有 C++ 版本仍保留:
cpp
// main.cpp:用最大子树链倍增求每个连通块重心,换根枚举每条边两侧组件。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 300005;
const int LOGN = 20;
int n;
vector<int> g[MAXN];
int subtree_size[MAXN], parent_node[MAXN], heavy_son[MAXN];
int jump_heavy[MAXN][LOGN]; // 沿“最大子树儿子”链向下跳
long long answer;
void rebuild_jump(int u) {
for (int i = 1; i < LOGN; i++) {
jump_heavy[u][i] = jump_heavy[jump_heavy[u][i - 1]][i - 1];
}
}
void add_centroid_sum(int root) {
int u = root;
// 沿最大子树方向向下跳,直到再往下会小于半棵树。
for (int i = LOGN - 1; i >= 0; i--) {
int v = jump_heavy[u][i];
if (v != 0 && subtree_size[v] * 2 >= subtree_size[root]) {
u = v;
}
}
answer += u;
if (subtree_size[u] * 2 == subtree_size[root]) {
answer += parent_node[u];
}
}
void dfs_prepare(int u, int fa) {
parent_node[u] = fa;
subtree_size[u] = 1;
heavy_son[u] = 0;
for (int i = 0; i < (int)g[u].size(); i++) {
int v = g[u][i];
if (v == fa) {
continue;
}
dfs_prepare(v, u);
subtree_size[u] += subtree_size[v];
if (subtree_size[v] > subtree_size[heavy_son[u]]) {
heavy_son[u] = v;
}
}
jump_heavy[u][0] = heavy_son[u];
rebuild_jump(u);
}
void dfs_reroot(int u, int fa) {
int first = 0;
int second = 0;
// 找出当前 u 的最大、次大相邻组件。换根时要临时排除正在走向的儿子。
for (int i = 0; i < (int)g[u].size(); i++) {
int v = g[u][i];
if (subtree_size[v] >= subtree_size[first]) {
second = first;
first = v;
} else if (subtree_size[v] >= subtree_size[second]) {
second = v;
}
}
for (int i = 0; i < (int)g[u].size(); i++) {
int v = g[u][i];
if (v == fa) {
continue;
}
// 删边 (u,v) 后,v 这一侧就是以 v 为根的连通块。
add_centroid_sum(v);
int old_size_u = subtree_size[u];
int old_size_v = subtree_size[v];
int old_parent_u = parent_node[u];
int old_parent_v = parent_node[v];
int old_jump_u = jump_heavy[u][0];
// 临时把 u 这一侧看成一个以 u 为根的连通块。
jump_heavy[u][0] = (v == first) ? second : first;
rebuild_jump(u);
subtree_size[u] -= subtree_size[v];
subtree_size[v] += subtree_size[u];
add_centroid_sum(u);
// 换根进入 v 的方向。
parent_node[u] = v;
parent_node[v] = fa;
dfs_reroot(v, u);
// 恢复现场。
parent_node[u] = old_parent_u;
parent_node[v] = old_parent_v;
subtree_size[u] = old_size_u;
subtree_size[v] = old_size_v;
jump_heavy[u][0] = old_jump_u;
rebuild_jump(u);
}
}
void solve_one() {
cin >> n;
for (int i = 1; i <= n; i++) {
g[i].clear();
subtree_size[i] = 0;
parent_node[i] = 0;
heavy_son[i] = 0;
for (int j = 0; j < LOGN; j++) {
jump_heavy[i][j] = 0;
}
}
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
answer = 0;
dfs_prepare(1, 0);
dfs_reroot(1, 0);
cout << answer << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
solve_one();
}
return 0;
}复杂度
预处理 O(n log n),每条边定位重心 O(log n),空间 O(n log n)。
总结
重心判定只看最大组件;把“沿最大组件走”做成倍增,换根就能在线完成。