[CSP-S 2019] 树的重心

GitHub跳转原题关系图返回列表

换根维护最大相邻组件,并沿最大子树链快速定位每个分割树的重心。

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)

总结

重心判定只看最大组件;把“沿最大组件走”做成倍增,换根就能在线完成。