题目描述

✅ 310. 最小高度树

image-20260928223404220

image-20260928223404221

题意分析

给定一棵无向树,可以选择任意节点作为根。以某节点为根时,树高是它到最远节点的距离,按边数计算;要求找出能让这个最大距离最小的所有根节点编号,返回顺序不限。

输入保证连通、无环,共有 n 个节点和 n - 1 条边,不需要删边或改变树形。改变的只是根的位置。最优根可能有一个,也可能有相邻的两个,不能只返回某个候选或最小高度数值。

解法:拓扑剥离叶子

核心思路

[!blue]

树超过两个节点时,叶子不会是最优根。若把根从叶子移到它唯一的邻居,到其他节点的路径都少一条边;到原叶子的距离虽然变成一,但原树至少还有第三个节点,整体最大距离仍会下降。因此可以先排除当前所有叶子。

同时删去一整层叶子后,对每个留下的节点而言,它到最远处的距离都恰好减少一:最远节点一定是叶子,去掉它后,通向它的父节点仍在剩余树中。所有候选的高度都减去相同值,谁更优的相对关系不变,所以可以继续在剩余树上重复这个过程。

这样不断从外向内剥离,最后留下一个或两个节点,就是树的中心。一个节点时它是唯一最优根;两个相连节点时二者处在同样居中的位置,都是答案。这也对应树的最长路径中间位置。

用邻接表记录连接关系,degree 表示节点当前剩余连接数量。初始把度数为一的节点放入队列;每删除一个叶子,就降低其邻居的度数,邻居首次降到一时成为下一层叶子,加入队列。

每轮开始必须固定本层叶子数量 size,整体扣除这批节点,再处理它们。新出现的叶子只能等到下一轮,不能刚剩两个就中断本轮,否则会留下未对称剥离的节点。只在整轮结束后检查 remaining > 2。

单节点树没有度数为一的叶子,需要直接返回零号节点。最后一轮中,唯一中心的度数可能先降到一而入队,再因其他叶子删除降到零;它仍应保留为答案,因此只在首次等于一时入队,不要求最后仍保持度数一。

解题步骤

  1. n == 1 时直接返回唯一节点零。
  2. 建立无向邻接表并统计度数,将所有度数为一的节点入队。
  3. 令 remaining = n;剩余超过两个节点时,固定本轮队列中的叶子数。
  4. 整体扣除这批叶子,逐个降低邻居度数;首次降为一的邻居加入下一层。
  5. 剩余一或两个节点时停止,返回队列中尚未处理的节点。

代码实现

class Solution {
    // 从所有叶子开始一层层删除,相当于从外向内收缩树的半径。
    public List<Integer> findMinHeightTrees(int n, int[][] edges) {
        List<Integer> res = new ArrayList<>();

        if (n == 1) {
            res.add(0);

            return res;
        }

        List<Integer>[] graph = new List[n];
        int[] degree = new int[n];

        for (int i = 0; i < n; i++) {
            graph[i] = new ArrayList<>();
        }

        for (int[] edge : edges) {
            int u = edge[0];
            int v = edge[1];

            graph[u].add(v);
            graph[v].add(u);
            degree[u]++;
            degree[v]++;
        }

        Queue<Integer> queue = new ArrayDeque<>();

        for (int i = 0; i < n; i++) {
            if (degree[i] == 1) {
                queue.offer(i);
            }
        }

        int remaining = n;

        while (remaining > 2) {
            // 固定当前层大小,新叶子留到下一层
            int size = queue.size();

            remaining -= size;

            for (int i = 0; i < size; i++) {
                int leaf = queue.poll();

                for (int next : graph[leaf]) {
                    degree[next]--;

                    // 只在首次降为一时入队,星形中心之后降为零也不重复加入
                    if (degree[next] == 1) {
                        queue.offer(next);
                    }
                }
            }
        }

        res.addAll(queue);

        return res;
    }
}
func findMinHeightTrees(n int, edges [][]int) []int {
    // 从所有叶子开始一层层删除,相当于从外向内收缩树的半径。
    if n == 1 {
        return []int{
            0,
        }
    }

    graph := make([][]int, n)
    degree := make([]int, n)
    for _, edge := range edges {
        u, v := edge[0], edge[1]
        graph[u] = append(graph[u], v)
        graph[v] = append(graph[v], u)
        degree[u]++
        degree[v]++
    }

    queue := make([]int, 0)
    for i := 0; i < n; i++ {
        if degree[i] == 1 {
            queue = append(queue, i)
        }
    }

    remaining := n
    head := 0
    for remaining > 2 {
        // 固定当前层大小,新叶子留到下一层
        size := len(queue) - head
        remaining -= size
        for i := 0; i < size; i++ {
            leaf := queue[head]
            head++
            for _, next := range graph[leaf] {
                degree[next]--
                // 只在首次降为一时入队,星形中心之后降为零也不重复加入
                if degree[next] == 1 {
                    queue = append(queue, next)
                }
            }
        }
    }

    return queue[head:]
}

复杂度分析

  • 时间复杂度:$O(n)$,树只有 n - 1 条边,每个节点最多入队一次,每条边的两端各被检查常数次。
  • 空间复杂度:$O(n)$,邻接表、度数数组和队列都与节点数同阶。

关键点总结

[!green]

  • 当前叶子作为根不如它的邻居,而同步去掉所有叶子不会改变剩余候选的优劣关系。
  • 一轮代表同一层的完整剥离,下一层不能提前混入。
  • 停在一个或两个中心,单节点与最终度数降到零的中心都要正确保留。

易错点总结

[!yellow]

  • 不断处理直到队列为空,会把应当返回的中心也删除。
  • 边删边看到剩余两个就立刻停止,可能只删除了本层的一部分,破坏同步剥离。
  • 使用不断增长的队列长度控制当前层,会把新叶子提前删除。
  • 度数小于等于一就反复入队,可能重复加入同一节点;只在首次降到一时加入。
  • 忽略 n == 1,会因为找不到度数为一的节点而返回空答案。
  • 只取队首一个结果,会漏掉双中心情况。

相似题目

题目 难度 关联与区别
543. 二叉树的直径 简单 树的最小高度根位于直径中心,可从最长路径理解不断剥叶子的正确性。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/37550675
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!