LeetCode 310. 最小高度树(BFS/拓扑排序)

116 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

题目

解法一:枚举根节点+DFS求树高(超时)

枚举以每个节点为根构成的树,然后求出该树的高度,所有树的最小高度即为答案,需要的时间复杂度为 O(n2)O(n^2)

算法流程:

  • 00n1n-1枚举根节点

    • 利用dfs求出以i(i[0,n1])i(i\in[0,n-1])为根节点的树高,并保存
  • 枚举所有树高,找出最小高度

class Solution {
    // 邻接表
    private Set<Integer>[] adj;
    // 以 i 为根节点时树的高度
    private int[] height;
    public List<Integer> findMinHeightTrees(int n, int[][] edges) {
        adj = new HashSet[n];
        for (int i = 0; i < n; i++) {
            adj[i] = new HashSet<>();
        }
        height = new int[n];
        // 获得邻接表
        for (int[] edge : edges) {
            int a = edge[0], b = edge[1];
            adj[a].add(b);
            adj[b].add(a);
        }
        int minHeight = n;
        // 枚举根节点并求得树高
        for (int i = 0; i < n; i++) {
            boolean[] visited = new boolean[n];
            height[i] = dfs(i, visited, 0);
            minHeight = Math.min(minHeight, height[i]);
        }
​
        // 枚举树高,找出最小高度
        List<Integer> res = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            if (minHeight == height[i]) {
                res.add(i);
            }
        }
        return res;
    }
    // 求以v为根节点的树高
    private int dfs(int v, boolean[] visited, int h) {
        visited[v] = true;
        int maxH = h;
        for (int w : adj[v]) {            
            if (!visited[w]) {
                maxH = Math.max(dfs(w, visited, h + 1), maxH);
            }
        }
        return maxH;
    }
}

解法二:广度优先搜索

根据题意,有以下结论成立:

设两个叶子节点xxyy的最长距离为 maxdist,可以得到结论最小高度树的高度为  maxdist2\ \Big \lceil \dfrac{\textit{maxdist}}{2} \Big \rceil,且最小高度树的根节点一定在 节点 xx 到节点 yy 的路径上。假设最长的路径的 m 个节点依次为 p1p2pmp_1 \rightarrow p_2 \rightarrow \cdots \rightarrow p_m,最长路径的长度为 m1m-1,则:

  • 如果 mm 为偶数,此时最小高度树的根节点为 pm2p_{\frac{m}{2}} 或者 pm2+1p_{\frac{m}{2}+1} ,且此时最小的高度为 m2\frac{m}{2}
  • 如果 mm 为奇数,此时最小高度树的根节点为 pm+12p_{\frac{m+1}{2}} ,且此时最小的高度为 m12\frac{m-1}{2}

上述结论的证明参见官方题解方法一

可以利用以下算法找到图中距离最远的两个节点与它们之间的路径:

  • 从任意节点 pp 出发 ,利用广度优先搜索或者深度优先搜索找到以 pp 为起点的最长路径的终点 xx
  • 从节点 xx 出发,找到以 xx 为起点的最长路径的终点 yy
  • xxyy 之间的路径即为图中的最长路径,找到路径的中间节点即为根节点

上述算法的证明可以参考「算法导论习题解答 9-1

算法流程:

  • 首先利用广度优先搜索找到距离节点 00 的最远节点 xx
  • 然后找到距离节点 xx 的最远节点 yy
  • 然后找到节点 xx 与节点 yy 的路径的最中间的节点即为最小高度树的根节点
class Solution {
    public List<Integer> findMinHeightTrees(int n, int[][] edges) {
        List<Integer> ans = new ArrayList<>();
        if (n == 1) {
            ans.add(0);
            return ans;
        }
        // 邻接表
        List<Integer>[] adj = new List[n];
        for (int i = 0; i < n; i++) {
            adj[i] = new ArrayList<>();
        }
        for (int[] edge : edges) {
            adj[edge[0]].add(edge[1]);
            adj[edge[1]].add(edge[0]);
        }
​
        // x->y路径中,存储每一个顶点的前一个顶点
        int[] parent = new int[n];
        Arrays.fill(parent, -1);
​
        // 找到与节点 0 最远的节点 x
        int x = findLongestNode(0, parent, adj);
​
        // 找到与节点 x 最远的节点 y
        int y = findLongestNode(x, parent, adj);
​
        // 求出节点 x 到节点 y 的路径
        List<Integer> path = findPath(parent, x, y);
        // path中没有加入节点x,此时path.size()就是路径长度
        int m = path.size();
        if (m % 2 == 0) {
            ans.add(path.get(m / 2 - 1));
        }
        ans.add(path.get(m / 2));
        return ans;
    }
​
    private List<Integer> findPath(int[] parent, int x, int y) {
        List<Integer> path = new ArrayList<>();
        parent[x] = -1;
        while (y != -1) {
            path.add(y);
            y = parent[y];
        }
        return path;
    }
​
    private int findLongestNode(int u, int[] parent, List<Integer>[] adj) {
        int n = adj.length;
        Queue<Integer> queue = new ArrayDeque<>();
        boolean[] visited = new boolean[n];
        queue.offer(u);
        visited[u] = true;
        // 距离节点u的最远节点
        int node = -1;
        while (!queue.isEmpty()) {
            int cur = queue.poll();
            node = cur;
            for (int v : adj[cur]) {
                if (!visited[v]) {
                    visited[v] = true;
                    parent[v] = cur;
                    queue.offer(v);
                }
            }
        }
        return node;
    }
}
  • 时间复杂度:O(n)O(n),其中 nn 是节点的个数
  • 空间复杂度:O(n)O(n)

解法三:拓扑排序

根据题意,越是靠里面的节点越有可能是最小高度树的根节点,距离两边相同的节点相当于把整个距离二分了,当然就是到两边距离最小的节点了

算法流程如下:

  • 首先找到所有度为 1 的节点压入队列,此时令节点剩余计数 remainNodes=nremainNodes=n
  • 同时将当前 remainNodesremainNodes 计数减去出度为 1 的节点数目,将最外层的度为 1 的叶子节点取出,并将与之相邻的节点的度减少,重复上述步骤将当前节点中度为 1 的节点压入队列中
  • 重复上述步骤,直到剩余的节点数组 remainNodes2remainNodes \le 2 时,此时剩余的节点即为当前高度最小树的根节点
class Solution {
    public List<Integer> findMinHeightTrees(int n, int[][] edges) {
        List<Integer> ans = new ArrayList<>();
        if (n == 1) {
            ans.add(0);
            return ans;
        }
        // 记录每个节点的度
        int[] degree = new int[n];
        // 邻接表
        List<Integer>[] adj = new List[n];
        for (int i = 0; i < n; i++) {
            adj[i] = new ArrayList<>();
        }
        for (int[] edge : edges) {
            adj[edge[0]].add(edge[1]);
            adj[edge[1]].add(edge[0]);
            degree[edge[0]]++;
            degree[edge[1]]++;
        }
​
        Queue<Integer> queue = new ArrayDeque<>();
        // 首先找到所有度为 1 的节点压入队列
        for (int i = 0; i < n; i++) {
            if (degree[i] == 1) {
                queue.offer(i);
            }
        }
        // 剩余节点数
        int remainNodes = n;
        while (remainNodes > 2) {
            int sz = queue.size();
            remainNodes -= sz;
            // 将最外层的度为 1 的叶子节点取出
            for (int i = 0; i < sz; i++) {
                int cur = queue.poll();
                for (int v : adj[cur]) {
                    // 将与之相邻的节点的度减少1
                    degree[v]--;
                    if (degree[v] == 1) {
                        queue.offer(v);
                    }
                }
            }
        }
        while (!queue.isEmpty()) {
            ans.add(queue.poll());
        }
        return ans;
    }
}
  • 时间复杂度:O(n)O(n),其中 nn 为节点的个数
  • 空间复杂度:O(n)O(n)

解法四:树形DP

参见:【宫水三叶】树形 DP 运用题

Reference

\