每日算法 — 使用java实现树重心:树的重心分解与路径统计优化

算法概述

树重心(Tree Centroid)是无向树中一个极其重要的节点:删除该节点后,剩余的所有连通子树中最大的节点数不超过原树节点数的一半。换言之,树重心是使”最大子树最小化”的节点。

树重心不仅是图论中的基础概念,更是点分治(Centroid Decomposition)算法的核心组件。点分治通过递归选取重心将树分解,可将许多 $O(n^2)$ 的树路径统计问题优化到 $O(n \log n)$,在算法竞赛和树形数据处理中具有广泛应用。

核心性质

性质1:存在性与数量

任意一棵树至少有一个、至多有两个重心。若树有两个重心,则它们必定相邻,且删除其中任一个后,另一节点成为剩余子树的重心。

性质2:子树大小约束

设树共有 $n$ 个节点,重心为 $c$。删除 $c$ 后,每棵子树的节点数不超过 $\lfloor n/2 \rfloor$。

性质3:递归分解深度

对一棵树不断选取重心进行分解,分解深度不超过 $O(\log n)$。这是点分治算法复杂度保证的来源。

性质4:路径必经性

树中任意两点间的简单路径必然经过它们的最近公共祖先(LCA)所在的重分解路径上的若干重心。

求树重心的算法

算法思路

通过一次DFS计算每个节点的子树大小(以该节点为根的子树包含的节点数),然后检查每个节点是否满足重心条件:删除该节点后,所有子树(包括”上方子树”,即整棵树减去当前子树)的最大值不超过 $n/2$。

Java完整实现

import java.util.*;

/**
 * 树重心(Tree Centroid)算法实现
 * 包含:重心查找、重心分解、路径统计应用
 */
public class TreeCentroid {

    private final int n;
    private final List<Integer>[] adj;
    private boolean[] removed;
    private int[] subtreeSize;
    private int[] parent;

    /**
     * 构造函数
     * @param n 节点数量(0-based编号)
     */
    @SuppressWarnings("unchecked")
    public TreeCentroid(int n) {
        this.n = n;
        this.adj = new ArrayList[n];
        for (int i = 0; i < n; i++) {
            adj[i] = new ArrayList<>();
        }
        this.removed = new boolean[n];
        this.subtreeSize = new int[n];
        this.parent = new int[n];
    }

    /**
     * 添加无向边
     */
    public void addEdge(int u, int v) {
        adj[u].add(v);
        adj[v].add(u);
    }

    /**
     * 计算以root为根的子树大小(不考虑已删除节点)
     * @param u 当前节点
     * @param p 父节点
     */
    private void calcSubtreeSize(int u, int p) {
        subtreeSize[u] = 1;
        for (int v : adj[u]) {
            if (v != p && !removed[v]) {
                calcSubtreeSize(v, u);
                subtreeSize[u] += subtreeSize[v];
            }
        }
    }

    /**
     * 在以entry为根的子树中查找重心
     * @param entry 子树的入口节点
     * @param p 父节点
     * @param totalSize 子树总节点数
     * @return 重心节点编号
     */
    private int findCentroid(int entry, int p, int totalSize) {
        for (int v : adj[entry]) {
            if (v != p && !removed[v] && subtreeSize[v] > totalSize / 2) {
                return findCentroid(v, entry, totalSize);
            }
        }
        return entry;
    }

    /**
     * 查找整棵树的重心
     * @return 重心节点编号
     */
    public int findCentroid() {
        calcSubtreeSize(0, -1);
        return findCentroid(0, -1, subtreeSize[0]);
    }

    /**
     * 查找指定节点所在连通块的重心
     */
    public int findCentroid(int entry) {
        calcSubtreeSize(entry, -1);
        return findCentroid(entry, -1, subtreeSize[entry]);
    }

    /**
     * 获取所有重心(1个或2个)
     */
    public List<Integer> findAllCentroids() {
        List<Integer> centroids = new ArrayList<>();
        calcSubtreeSize(0, -1);
        int total = subtreeSize[0];

        findAllCentroidsDFS(0, -1, total, centroids);
        return centroids;
    }

    private void findAllCentroidsDFS(int u, int p, int total, List<Integer> result) {
        boolean isCentroid = true;
        int maxPart = total - subtreeSize[u]; // "上方"子树大小

        for (int v : adj[u]) {
            if (v != p && !removed[v]) {
                maxPart = Math.max(maxPart, subtreeSize[v]);
                if (subtreeSize[v] > total / 2) {
                    isCentroid = false;
                    findAllCentroidsDFS(v, u, total, result);
                }
            }
        }

        if (isCentroid && maxPart <= total / 2) {
            result.add(u);
        }
    }

    // ==================== 重心分解 ====================

    /**
     * 重心分解树节点信息
     */
    public static class DecompositionNode {
        int centroid;           // 当前重心
        int parentCentroid;     // 父重心(根为-1)
        int depth;              // 分解深度
        List<Integer> nodes;    // 该层包含的原始节点

        DecompositionNode(int centroid, int parent, int depth) {
            this.centroid = centroid;
            this.parentCentroid = parent;
            this.depth = depth;
            this.nodes = new ArrayList<>();
        }
    }

    private List<DecompositionNode> decompositionTree;
    private int[] centroidParent;  // 每个节点在重心分解树中的父重心
    private int[] centroidDepth;   // 每个节点在重心分解树中的深度

    /**
     * 执行重心分解
     * @return 分解树的层次结构
     */
    public List<DecompositionNode> decompose() {
        decompositionTree = new ArrayList<>();
        centroidParent = new int[n];
        centroidDepth = new int[n];
        Arrays.fill(centroidParent, -1);
        Arrays.fill(centroidDepth, 0);
        removed = new boolean[n]; // 重置删除标记

        decomposeRecursive(0, -1, 0);
        return decompositionTree;
    }

    /**
     * 递归分解子树
     * @param entry 子树入口
     * @param parentCentroid 父重心
     * @param depth 当前分解深度
     */
    private void decomposeRecursive(int entry, int parentCentroid, int depth) {
        // 计算子树大小
        calcSubtreeSize(entry, -1);
        int total = subtreeSize[entry];

        // 找到重心
        int centroid = findCentroid(entry, -1, total);

        // 标记删除
        removed[centroid] = true;
        centroidParent[centroid] = parentCentroid;
        centroidDepth[centroid] = depth;

        DecompositionNode node = new DecompositionNode(centroid, parentCentroid, depth);
        collectNodes(centroid, -1, node.nodes);
        decompositionTree.add(node);

        // 递归分解每个子连通块
        for (int v : adj[centroid]) {
            if (!removed[v]) {
                decomposeRecursive(v, centroid, depth + 1);
            }
        }
    }

    /**
     * 收集连通块中的所有节点
     */
    private void collectNodes(int u, int p, List<Integer> nodes) {
        nodes.add(u);
        for (int v : adj[u]) {
            if (v != p && !removed[v]) {
                collectNodes(v, u, nodes);
            }
        }
    }

    // ==================== 应用:统计路径和为K的路径数 ====================

    private int pathCountK;
    private int targetK;
    private int[] nodeValues;

    /**
     * 统计树中路径和等于K的路径数量(路径可从一个节点到另一个节点)
     * 使用重心分解优化到 O(n log n)
     */
    public int countPathsWithSum(int[] values, int K) {
        this.nodeValues = values;
        this.targetK = K;
        this.pathCountK = 0;
        removed = new boolean[n];

        countPathsDecompose(0);
        return pathCountK;
    }

    private void countPathsDecompose(int entry) {
        calcSubtreeSize(entry, -1);
        int total = subtreeSize[entry];
        int centroid = findCentroid(entry, -1, total);

        removed[centroid] = true;

        // 统计经过重心的路径
        Map<Integer, Integer> sumCount = new HashMap<>();
        sumCount.put(nodeValues[centroid], 1);

        for (int v : adj[centroid]) {
            if (removed[v]) continue;

            List<Integer> pathSums = new ArrayList<>();
            collectPathSums(v, centroid, nodeValues[centroid] + nodeValues[v], pathSums);

            // 统计经过重心的路径
            for (int sum : pathSums) {
                int need = targetK - sum + nodeValues[centroid];
                pathCountK += sumCount.getOrDefault(need, 0);
            }

            // 将当前子树的路径和加入哈希表
            for (int sum : pathSums) {
                sumCount.merge(sum, 1, Integer::sum);
            }
        }

        // 递归处理子连通块
        for (int v : adj[centroid]) {
            if (!removed[v]) {
                countPathsDecompose(v);
            }
        }
    }

    /**
     * 收集从start出发到各节点的路径和
     */
    private void collectPathSums(int u, int p, int currentSum, List<Integer> sums) {
        sums.add(currentSum);
        for (int v : adj[u]) {
            if (v != p && !removed[v]) {
                collectPathSums(v, u, currentSum + nodeValues[v], sums);
            }
        }
    }

    // ==================== 工具方法 ====================

    /**
     * 获取重心分解树中某节点的父重心
     */
    public int getCentroidParent(int node) {
        return centroidParent[node];
    }

    /**
     * 获取重心分解树中某节点的深度
     */
    public int getCentroidDepth(int node) {
        return centroidDepth[node];
    }

    /**
     * 打印树的结构
     */
    public void printTree() {
        System.out.println("Tree structure (adjacency list):");
        for (int i = 0; i < n; i++) {
            System.out.println("  Node " + i + " -> " + adj[i]);
        }
    }

    /**
     * 打印重心分解结果
     */
    public void printDecomposition() {
        if (decompositionTree == null) {
            System.out.println("Please run decompose() first");
            return;
        }
        System.out.println("Centroid Decomposition:");
        for (DecompositionNode node : decompositionTree) {
            System.out.printf("  Depth %d: centroid=%d, parent=%d, nodes=%s%n",
                node.depth, node.centroid, node.parentCentroid, node.nodes);
        }
    }

    public static void main(String[] args) {
        // 示例1:基本重心查找
        System.out.println("=== Example 1: Basic Centroid ===");
        TreeCentroid tc1 = new TreeCentroid(7);
        tc1.addEdge(0, 1);
        tc1.addEdge(1, 2);
        tc1.addEdge(1, 3);
        tc1.addEdge(3, 4);
        tc1.addEdge(3, 5);
        tc1.addEdge(5, 6);

        tc1.printTree();
        int centroid1 = tc1.findCentroid();
        System.out.println("Centroid: " + centroid1);
        System.out.println("All centroids: " + tc1.findAllCentroids());

        // 示例2:重心分解
        System.out.println("\n=== Example 2: Centroid Decomposition ===");
        TreeCentroid tc2 = new TreeCentroid(10);
        tc2.addEdge(0, 1);
        tc2.addEdge(1, 2);
        tc2.addEdge(1, 3);
        tc2.addEdge(3, 4);
        tc2.addEdge(3, 5);
        tc2.addEdge(5, 6);
        tc2.addEdge(2, 7);
        tc2.addEdge(2, 8);
        tc2.addEdge(8, 9);

        tc2.decompose();
        tc2.printDecomposition();

        // 示例3:双重心情况
        System.out.println("\n=== Example 3: Two Centroids ===");
        TreeCentroid tc3 = new TreeCentroid(4);
        tc3.addEdge(0, 1);
        tc3.addEdge(1, 2);
        tc3.addEdge(2, 3); // 路径图 0-1-2-3
        System.out.println("All centroids of path graph: " + tc3.findAllCentroids());

        // 示例4:路径和统计
        System.out.println("\n=== Example 4: Path Sum Count ===");
        TreeCentroid tc4 = new TreeCentroid(6);
        tc4.addEdge(0, 1);
        tc4.addEdge(1, 2);
        tc4.addEdge(1, 3);
        tc4.addEdge(3, 4);
        tc4.addEdge(3, 5);

        int[] values = {1, 2, 3, 4, 5, 6};
        int K = 7;
        int count = tc4.countPathsWithSum(values, K);
        System.out.println("Paths with sum " + K + ": " + count);
        // 手动验证:1+2+4=7 (0-1-3), 1+2+3=6不对, 3+4=7 (2-1-3), 1+6=7 (0-1-3-5不对)
        // 2+5=7 (1-3-4), 1+2+4=7 (0-1-3)

        // 示例5:星形树的重心
        System.out.println("\n=== Example 5: Star Tree ===");
        TreeCentroid tc5 = new TreeCentroid(5);
        tc5.addEdge(0, 1);
        tc5.addEdge(0, 2);
        tc5.addEdge(0, 3);
        tc5.addEdge(0, 4);
        System.out.println("Star tree centroid: " + tc5.findCentroid());
        System.out.println("All centroids: " + tc5.findAllCentroids());

        // 示例6:性能测试
        System.out.println("\n=== Example 6: Performance Test ===");
        int size = 100000;
        TreeCentroid tc6 = new TreeCentroid(size);
        Random rand = new Random(42);

        // 构建随机树
        for (int i = 1; i < size; i++) {
            tc6.addEdge(i, rand.nextInt(i));
        }

        long start = System.currentTimeMillis();
        int c6 = tc6.findCentroid();
        long findTime = System.currentTimeMillis() - start;
        System.out.println("Tree size: " + size);
        System.out.println("Centroid: " + c6);
        System.out.println("Find centroid time: " + findTime + "ms");

        start = System.currentTimeMillis();
        tc6.decompose();
        long decomposeTime = System.currentTimeMillis() - start;
        System.out.println("Decompose time: " + decomposeTime + "ms");
        System.out.println("Decomposition levels: " + tc6.decompositionTree.size());

        // 路径统计性能
        int[] largeValues = new int[size];
        for (int i = 0; i < size; i++) {
            largeValues[i] = rand.nextInt(100) + 1;
        }
        start = System.currentTimeMillis();
        int pathCount = tc6.countPathsWithSum(largeValues, 500);
        long pathTime = System.currentTimeMillis() - start;
        System.out.println("Path count (K=500): " + pathCount);
        System.out.println("Path count time: " + pathTime + "ms");
    }
}

关键要点解析

1. 重心条件的等价表述

节点 $u$ 是重心的充要条件是:对于 $u$ 的每个邻接子树 $v$,$size(v) \leq n/2$,且”上方子树”(即 $n – size(u)$)也满足 $\leq n/2$。在DFS中,$size(u)$ 是以 $u$ 为根的子树大小,因此”上方子树”自然为 $n – size(u)$。

2. 重心分解的递归深度保证

每次删除重心后,每棵剩余子树的大小不超过 $n/2$。因此递归深度满足 $T(n) = T(n/2) + O(n)$,由主定理得 $T(n) = O(n \log n)$,分解层数不超过 $\log_2 n$。

3. 路径统计中的容斥原理

在 countPathsWithSum 中,先统计经过重心的所有路径,再递归处理各子树。为避免重复统计完全位于单棵子树内的路径,采用”先统计、后加入”的策略:用哈希表记录已处理子树的路径和,当前子树只与之前子树组合统计,处理完后再将当前子树加入哈希表。

复杂度分析

操作 时间复杂度 空间复杂度
单次重心查找 $O(n)$ $O(n)$
重心分解 $O(n \log n)$ $O(n \log n)$
路径统计(点分治) $O(n \log n)$ $O(n)$

应用场景

  • 树路径查询:统计满足特定条件(长度、权值和、边数等)的路径数量
  • 最近点对变种:树上的距离约束问题
  • 动态树维护:结合重心分解处理树的动态修改查询
  • 树的中心定位:通信网络中选取最优服务器位置
  • 树形DP优化:将 $O(n^2)$ 的树形动态规划优化到 $O(n \log n)$

树重心将”复杂树结构”分解为”对数层级的简单子树”,是连接基础图论与高级算法设计的桥梁。