LeetCode 第 310 题:“最小高度树”题解

题解地址:贪心法:根据拓扑排序的思路(Python 代码、Java 代码)

说明:文本首发在力扣的题解版块,更新也会在第 1 时间在上面的网站中更新,这篇文章只是上面的文章的一个快照,您可以点击上面的链接看到其他网友对本文的评论。

传送门:310. 最小高度树

对于一个具有树特征的无向图,我们可选择任何一个节点作为根。图因此可以成为树,在所有可能的树中,具有最小高度的树被称为最小高度树。给出这样的一个图,写出一个函数找到所有的最小高度树并返回他们的根节点。

格式

该图包含 n 个节点,标记为 0 到 n - 1。给定数字 n 和一个无向边 edges 列表(每一个边都是一对标签)。

你可以假设没有重复的边会出现在 edges 中。由于所有的边都是无向边, [0, 1]和 [1, 0] 是相同的,因此不会同时出现在 edges 里。

示例 1:

输入: n = 4, edges = [[1, 0], [1, 2], [1, 3]]

   0
   |
   1
  / \
 2   3 

输出: [1] 示例 2:

输入: n = 6, edges = [[0, 3], [1, 3], [2, 3], [4, 3], [5, 4]]

0  1  2
 \ | /
   3
   |
   4
   |
   5 

输出: [3, 4] 说明:

根据树的定义,树是一个无向图,其中任何两个顶点只通过一条路径连接。 换句话说,一个任何没有简单环路的连通图都是一棵树。 树的高度是指根节点和叶子节点之间最长向下路径上边的数量。

贪心法:根据拓扑排序的思路(Python 代码、Java 代码)

思路分析

这道题一开始给我的感觉特别像拓扑排序,做下来,感觉它们的本质是一样的,更深层次的思想是贪心算法。

直觉上,一棵树越靠“外面”的结点,我们越不可能把它作为根结点,如果这样做的话,可能树的高度是很高的,例如下面这张图。

image.png

因此,我们使用“剔除边缘结点”的策略,这里的边缘结点就是指连接其它结点最少的结点,用专业的名词来说,就是指向它的结点最少的结点,“入度”最少的结点,为此,我们可以画几张图看一下。

(下面这这张图,我画得很潦草,我不想说清楚画这张图表示的意思,而是想说,有的时候分析问题,需要自己动手,比看别人的思路的理解要深刻。) image.png

画完这张图,我们能归纳出,结点最后只会剩下 1 个或者 2 个。如果对这个结论还不确定的朋友,不妨多画几张图,把结点个数为 6 个 、7 个时候的情况也考虑一下。

综上所述,总结一下我们的算法:每次总是删除“入度”个数最少的结点,因为树是无向无环图,删除了它们以后,与之相连的结点的入度也相应地减少 1,直到最后剩下 1 个结点或者 2 个结点。

在编码的时候,我借鉴了“拓扑排序”的代码,使用了“邻接表”表示图,使用了“入度数组”,还使用了队列保存了下一轮要“剔除”的结点编号。关于拓扑排序的知识和代码实现,可以参考「力扣」第 207 题:课程表「力扣」第 210 题:课程表 II

参考代码

Python 代码:

class Solution:

    # 贪心法

    def findMinHeightTrees(self, n, edges):

        if n <= 2:
            return [i for i in range(n)]

        from collections import defaultdict
        from collections import deque

        in_degrees = [0] * n
        # True 表示保留,如果设置为 False 则表示删除
        res = [True] * n
        # 邻接表
        adjs = defaultdict(list)
        for edge in edges:
            in_degrees[edge[0]] += 1
            in_degrees[edge[1]] += 1
            adjs[edge[0]].append(edge[1])
            adjs[edge[1]].append(edge[0])

        deque = deque()

        for i in range(n):
            if in_degrees[i] == 1:
                deque.append(i)

        # 根据示例,可知,最后可能剩下 1 个结点或者 2 个结点
        # 或者自己画一个图也能分析出来
        while n > 2:
            size = len(deque)
            # 一次减去当前队列这么多个结点
            n -= size
            for i in range(size):
                top = deque.popleft()
                res[top] = False

                successors = adjs[top]
                # 它和它的邻接点的入度全部减 1
                successors.append(top)

                for item in successors:
                    # 一个结点的入度重复减是没有关系的
                    # 我们只关心在最边界的那个结点,把它移除,所以可以认为是贪心法
                    in_degrees[item] -= 1

                    if in_degrees[item] == 1:
                        deque.append(item)

        return [i for i in range(len(res)) if res[i]]

Java 代码:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

public class Solution {


    public List<Integer> findMinHeightTrees(int n, int[][] edges) {
        List<Integer> res = new ArrayList<>();
        // 特判
        if (n <= 2) {
            for (int i = 0; i < n; i++) {
                res.add(i);
            }
            return res;
        }

        // 入度数组,每一次要把入度为 1 的结点剔除
        int[] inDegrees = new int[n];

        // 默认为 False,如果剔除,设置为 True
        boolean[] result = new boolean[n];

        // 因为是无向图,所以邻接表拿出一条边,两个结点都要存一下
        // 注意:右边就不要写具体的实现类了,等到实例化的时候再写具体的实现类
        Set<Integer>[] adjs = new Set[n];
        // 初始化
        for (int i = 0; i < n; i++) {
            adjs[i] = new HashSet<>();
        }

        for (int[] edge : edges) {
            int start = edge[0];
            int end = edge[1];
            adjs[start].add(end);
            adjs[end].add(start);
            inDegrees[start] += 1;
            inDegrees[end] += 1;
        }
        LinkedList<Integer> queue = new LinkedList<>();

        // 入度为 1 的结点入队
        for (int i = 0; i < n; i++) {
            if (inDegrees[i] == 1) {
                queue.addLast(i);
            }
        }

        // 注意边界条件 n == 2 和 n == 1 是如何分析出来的
        while (n > 2) {
            int size = queue.size();
            System.out.println(queue);
            // 一次减去这么多
            n -= size;
            for (int i = 0; i < size; i++) {
                int top = queue.removeFirst();
                result[top] = true;
                inDegrees[top] -= 1;
                // 把它和它的邻接结点的入度全部减 1
                Set<Integer> successors = adjs[top];
                for (Integer successor : successors) {
                    inDegrees[successor] -= 1;
                    if (inDegrees[successor] == 1) {
                        queue.addLast(successor);
                    }
                }
            }
        }
        n = result.length;
        for (int i = 0; i < n; i++) {
            if (!result[i]) {
                res.add(i);
            }
        }
        return res;
    }


    public static void main(String[] args) {
        int[][] edges = new int[][]{{1, 0}, {1, 2}, {1, 3}};
        int n = 4;
        Solution solution = new Solution();
        List<Integer> res = solution.findMinHeightTrees(n, edges);
        System.out.println(res);
    }
}