每日算法 — 使用java实现树的中心:换根动态规划与直径计算

问题引入

在树形结构中,”树的中心”是指使得到所有其他节点的最远距离最小的节点(可以有一个或两个中心)。与”树的重心”(使最大子树节点数最小)不同,中心关注的是距离度量。求树的中心需要先理解树的直径——树中最长的一条简单路径。本文用Java实现两次DFS求直径、换根DP求每个节点最远距离,最终定位树的中心,并扩展到树的半径与偏心距概念。

算法原理

树的直径

树的直径可通过两次DFS(或BFS)求得:
1. 从任意节点 u 出发,找到最远的节点 v
2. 从 v 出发,找到最远的节点 w
3. 路径 v-w 即为树的直径

证明:设直径为 a-b。若第一次DFS从任意点出发到达的最远点不是 a 或 b,则可通过三角不等式导出矛盾。

换根DP求所有节点最远距离

若仅求直径,两次DFS足够。但要找树的中心,需知道每个节点到所有其他节点的最远距离(偏心距)。换根DP通过两次遍历优雅解决:

第一次DFS(自底向上):down[u] 表示从 u 向下走的最长距离(到子树中最远叶子的距离)。

down[u] = max(down[v] + w(u,v)) for all child v of u

第二次DFS(自顶向下):up[u] 表示从 u 向上走(经过父节点)的最长距离。对于 u 的子节点 v:

up[v] = max(up[u], 次大down值 + w(u,parent)) + w(u,v)

节点 u 的偏心距为 max(down[u], up[u]),偏心距最小的节点即为树的中心。

Java完整实现

import java.util.*;

/**
 * 树的中心求解:直径 + 换根动态规划
 */
public class TreeCenter {

    static class Edge {
        int to, weight;
        Edge(int to, int weight) {
            this.to = to;
            this.weight = weight;
        }
    }

    private List<Edge>[] graph;
    private int n;
    private int[] down;    // 向下的最长距离
    private int[] down2;   // 向下的次长距离
    private int[] up;      // 向上的最长距离
    private int[] parent;  // 记录父节点
    private int[] parentEdgeWeight;

    public TreeCenter(int n) {
        this.n = n;
        graph = new ArrayList[n];
        for (int i = 0; i < n; i++) graph[i] = new ArrayList<>();
        down = new int[n];
        down2 = new int[n];
        up = new int[n];
        parent = new int[n];
        parentEdgeWeight = new int[n];
    }

    public void addEdge(int u, int v, int w) {
        graph[u].add(new Edge(v, w));
        graph[v].add(new Edge(u, w));
    }

    /**
     * 第一次DFS:计算down值(自底向上)
     */
    private void dfs1(int u, int p) {
        parent[u] = p;
        down[u] = 0;
        down2[u] = 0;

        for (Edge e : graph[u]) {
            int v = e.to;
            if (v == p) continue;
            parentEdgeWeight[v] = e.weight;
            dfs1(v, u);

            int dist = down[v] + e.weight;
            if (dist > down[u]) {
                down2[u] = down[u];
                down[u] = dist;
            } else if (dist > down2[u]) {
                down2[u] = dist;
            }
        }
    }

    /**
     * 第二次DFS:计算up值(自顶向下)
     */
    private void dfs2(int u, int p) {
        for (Edge e : graph[u]) {
            int v = e.to;
            if (v == p) continue;

            // up[v] 来自两部分:up[u](继续向上)或 u 的down(转向另一子树)
            int useDown = (down[u] == down[v] + e.weight) ? down2[u] : down[u];
            up[v] = Math.max(up[u], useDown) + e.weight;

            dfs2(v, u);
        }
    }

    /**
     * 求树的中心
     * @return 中心节点列表(一个或两个)
     */
    public List<Integer> findCenter() {
        dfs1(0, -1);
        dfs2(0, -1);

        int minEccentricity = Integer.MAX_VALUE;
        List<Integer> centers = new ArrayList<>();

        for (int i = 0; i < n; i++) {
            int ecc = Math.max(down[i], up[i]);
            if (ecc < minEccentricity) {
                minEccentricity = ecc;
                centers.clear();
                centers.add(i);
            } else if (ecc == minEccentricity) {
                centers.add(i);
            }
        }

        return centers;
    }

    /**
     * 求树的直径(两次DFS/BFS法)
     * @return 直径长度
     */
    public int findDiameter() {
        int[] dist = bfs(0);
        int farNode = 0;
        for (int i = 1; i < n; i++) {
            if (dist[i] > dist[farNode]) farNode = i;
        }

        dist = bfs(farNode);
        int diameter = 0;
        for (int d : dist) diameter = Math.max(diameter, d);

        return diameter;
    }

    /**
     * BFS求单源最短路(树中即为距离)
     */
    private int[] bfs(int start) {
        int[] dist = new int[n];
        Arrays.fill(dist, -1);
        Queue<Integer> q = new LinkedList<>();
        q.offer(start);
        dist[start] = 0;

        while (!q.isEmpty()) {
            int u = q.poll();
            for (Edge e : graph[u]) {
                int v = e.to;
                if (dist[v] == -1) {
                    dist[v] = dist[u] + e.weight;
                    q.offer(v);
                }
            }
        }
        return dist;
    }

    /**
     * 打印每个节点的偏心距
     */
    public void printEccentricities() {
        System.out.println("节点信息:");
        System.out.println("节点\tdown\tup\t偏心距");
        for (int i = 0; i < n; i++) {
            int ecc = Math.max(down[i], up[i]);
            System.out.printf("%d\t%d\t%d\t%d%n", i, down[i], up[i], ecc);
        }
    }

    /**
     * 辅助方法:构建示例树
     *        0
     *       /|\ 
     *      1 2 3
     *     /     \
     *    4       5
     *   /
     *  6
     */
    public static TreeCenter createExample() {
        TreeCenter tree = new TreeCenter(7);
        tree.addEdge(0, 1, 1);
        tree.addEdge(0, 2, 1);
        tree.addEdge(0, 3, 1);
        tree.addEdge(1, 4, 1);
        tree.addEdge(3, 5, 1);
        tree.addEdge(4, 6, 1);
        return tree;
    }

    public static void main(String[] args) {
        TreeCenter tree = createExample();

        System.out.println("=== 树的中心求解演示 ===\n");

        int diameter = tree.findDiameter();
        System.out.println("树的直径: " + diameter);

        List<Integer> centers = tree.findCenter();
        System.out.println("树的中心: " + centers);

        tree.printEccentricities();

        // 测试权重不同的树
        System.out.println("\n=== 带权树测试 ===");
        TreeCenter weightedTree = new TreeCenter(5);
        weightedTree.addEdge(0, 1, 3);
        weightedTree.addEdge(0, 2, 1);
        weightedTree.addEdge(2, 3, 2);
        weightedTree.addEdge(2, 4, 4);

        System.out.println("带权树直径: " + weightedTree.findDiameter());
        System.out.println("带权树中心: " + weightedTree.findCenter());
        weightedTree.printEccentricities();
    }
}

复杂度分析

操作 时间复杂度 空间复杂度
求直径(两次BFS) O(n) O(n)
换根DP(两次DFS) O(n) O(n)
整体求中心 O(n) O(n)

所有操作均为线性时间,这是因为树没有环,每个边和节点仅被访问常数次。

扩展思考

  1. 树的半径:树的半径等于直径的一半(向上取整),即中心节点的偏心距。半径与直径满足 diameter ≤ 2 × radius。
  2. 多中心情况:当直径长度为奇数时,树有两个中心(直径中点两侧各一个);为偶数时有一个中心。
  3. 动态树维护:若树需要支持加边/删边操作并实时查询中心,可用 Link-Cut Tree 维护树的直径端点,每次操作后重新计算中心。
  4. 最小高度树:在图中(而非树中)找到使得到所有节点最大距离最小的节点集,称为最小高度树。对于一般图,中心可能不止两个,且需用Floyd-Warshall或多次BFS求解。

换根动态规划是解决”以每个节点为根时的子树信息”类问题的通用技巧。其核心思想是:先通过一次DFS计算子树内的信息(down),再通过一次DFS利用父节点的信息推导子树外的信息(up)。掌握这套”先下后上”的思维范式,将帮助你轻松应对树形DP中的绝大多数难题。