LeetCode 310-最小高度树

在这里插入图片描述

方法一:广度优先搜索

思路与算法

题目中给定的含有 n n n 个节点的树,可以推出含有以下特征:

  • 任意两个节点之间有且仅有一条路径;
  • 树中共有 n − 1 n-1 n1 条不同的边;
  • 叶子节点的度为 1 1 1,非叶子节点的度至少为 2 2 2
  • 树的高度由根节点到叶子节点的最大距离决定。

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

d i s t [ x ] [ y ] dist[x][y] dist[x][y] 表示从节点 x x x 到节点 y y y 的距离,假设树中距离最长的两个节点为 ( x , y ) (x,y) (x,y),它们之间的距离为 maxdist = d i s t [ x ] [ y ] \text{maxdist}=dist[x][y] maxdist=dist[x][y],则可以推出以任意节点构成的树最小高度一定为 minheight = ⌈ maxdist 2 ⌉ \text{minheight}=\lceil \frac{\text{maxdist}}{2}\rceil minheight=2maxdist,且最小高度的树的根节点一定在节点 x x x 到节点 y y y 的路径上(即以路径上的中心节点作为根节点)。

假设最长路径的 m m m 个节点依次为 p 1 → p 2 → ⋯ → p m p_1\rightarrow p_2\rightarrow\cdots\rightarrow p_m p1p2pm,最长路径的长度为 m − 1 m-1 m1,可以得到以下结论:

  • m m m 为偶数,此时最小高度树的根节点为 p m 2 p_{\frac{m}{2}} p2m p m 2 + 1 p_{\frac{m}{2}+1} p2m+1,且此时最小高度为 m 2 \frac{m}{2} 2m
  • m m m 为奇数,此时最小高度树的根节点为 p m + 1 2 p_{\frac{m+1}{2}} p2m+1,且此时的最小高度为 m − 1 2 \frac{m-1}{2} 2m1

因此,我们只需要求出路径最长的两个叶子节点,并求出其路径的中心节点即为最小高度树的根节点。

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

  • 以任意节点 p p p 出现,利用广度优先搜索或者深度优先搜索找到以 p p p 为起点的最长路径的终点 x x x
  • 以节点 x x x 出发,找到以 x x x 为起点的最长路径的终点 y y y
  • x x x y y y 之间的路径即为图中的最长路径,找到路径的中间节点即为根节点。
代码
class Solution {
    public List<Integer> findMinHeightTrees(int n, int[][] edges) {
        List<Integer> ans = new ArrayList<Integer>();
        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<Integer>();
        }
        for(int[] edge: edges){                         //初始化邻接表
            adj[edge[0]].add(edge[1]);
            adj[edge[1]].add(edge[0]);
        }
        int[] parent = new int[n];                      //父亲数组
        Arrays.fill(parent, -1);

        int x = findLongestNode(0, parent, adj);        //找到与节点 0 最远的节点 x
        int y = findLongestNode(x, parent, adj);        //找到与节点 x 最远的节点 y
        List<Integer> path = new ArrayList<Integer>();  //求出节点 x 到节点 y 的路径
        parent[x] = -1;                                 //x为根节点,父节点置为-1
        while (y != -1) {                               //当y存在,就将最远路径(x->y)记录下来
            path.add(y);
            y = parent[y];
        }
        int m = path.size();                            //m为最长路径的节点数
        if (m % 2 == 0) {                               //m为偶数
            ans.add(path.get(m / 2 - 1));               //减1是因为节点编号从0开始
        }
        ans.add(path.get(m / 2));                       //m为奇数
        return ans;
    }

    /*
     * 利用广度优先算法,以节点u为根节点,找到距离其最远的叶子节点
     */
    public int findLongestNode(int u, int[] parent, List<Integer>[] adj) {
        int n = adj.length;
        Queue<Integer> queue = new ArrayDeque<Integer>();
        boolean[] visit = new boolean[n];                //访问数组,防止重复访问
        queue.offer(u);
        visit[u] = true;
        int node = -1;
  
        while (!queue.isEmpty()) {
            int curr = queue.poll();
            node = curr;
            for (int v : adj[curr]) {                    //依次遍历当前节点的邻接点
                if (!visit[v]) {                         //若未被访问过,则访问,并更新父亲数组
                    visit[v] = true;
                    parent[v] = curr;
                    queue.offer(v);
                }
            }
        }
        return node;
    }
}

方法二:深度优先搜索

方法一中使用广度优先搜索求出路径最长的节点与路径,我们还可以使用深度优先搜索来实现。首先找到距离节点 0 0 0 的最远节点 x x x,然后找到距离节点 x x x 的最远节点 y y y,然后找到节点 x x x 与节点 y y y 的路径,然后找到根节点。

class Solution {
    public List<Integer> findMinHeightTrees(int n, int[][] edges) {
        List<Integer> ans = new ArrayList<Integer>();
        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<Integer>();
        }
        for(int[] edge: edges){                         //初始化邻接表
            adj[edge[0]].add(edge[1]);
            adj[edge[1]].add(edge[0]);
        }
        int[] parent = new int[n];                      //父亲数组
        Arrays.fill(parent, -1);

        int x = findLongestNode(0, parent, adj);        //找到与节点 0 最远的节点 x
        int y = findLongestNode(x, parent, adj);        //找到与节点 x 最远的节点 y
        List<Integer> path = new ArrayList<Integer>();  //求出节点 x 到节点 y 的路径
        parent[x] = -1;                                 //x为根节点,父节点置为-1
        while (y != -1) {                               //当y存在,就将最远路径(x->y)记录下来
            path.add(y);
            y = parent[y];
        }
        int m = path.size();                            //m为最长路径的节点数
        if (m % 2 == 0) {                               //m为偶数
            ans.add(path.get(m / 2 - 1));               //减1是因为节点编号从0开始
        }
        ans.add(path.get(m / 2));                       //m为奇数
        return ans;
    }

    /*
     * 利用深度优先算法,以节点u为根节点,找到距离其最远的叶子节点
     */
    public int findLongestNode(int u, int[] parent, List<Integer>[] adj) {
        int n = adj.length;
        int[] dist = new int[n];                        //记录每个节点到节点u的距离
        Arrays.fill(dist, -1);
        dist[u] = 0;
        dfs(u, dist, parent, adj);                      //深度优先来更新距离数组
        int maxdist = 0;
        int node = -1;
        for(int i = 0; i < n; i++){                     //寻找最大距离以及对应节点
            if(dist[i] > maxdist){
                maxdist = dist[i];
                node = i;
            }
        }
        return node;
    }

    public void dfs(int u, int[] dist, int[] parent, List<Integer>[] adj) {
        for (int v : adj[u]) {
            if (dist[v] < 0) {
                dist[v] = dist[u] + 1;
                parent[v] = u;
                dfs(v, dist, parent, adj); 
            }
        }
    }
}

方法三:拓扑排序

思路与算法

由于树的高度由根节点到叶子节点之间的最大距离构成,假设树中距离最长的两个节点为 ( x , y ) (x,y) (x,y),它们之间的距离为 maxdist = d i s t [ x ] [ y ] \text{maxdist}=dist[x][y] maxdist=dist[x][y],假设 x x x y y y 的路径为 x → p 1 → p 2 → ⋯ → p k − 1 → p k → y x\rightarrow p_1\rightarrow p_2\rightarrow\cdots\rightarrow p_{k-1}\rightarrow p_k\rightarrow y xp1p2pk1pky,由方法一可知最小树的根节点一定为该路径中的中间节点,我们尝试删除最外层的度为 1 1 1 的节点 x , y x,y x,y 后,则可以知道路径中与 x , y x,y x,y 相邻的节点 p 1 , p k p_1,p_k p1,pk 此时也变为度为 1 1 1 的节点,此时我们再次删除最外层度为 1 1 1 的节点直到剩下根节点为止。

算法步骤如下:

  • 首先找到所有度为 1 1 1 的节点压入队列,此时令节点剩余计数 r e m a i n N o d e s = n remainNodes=n remainNodes=n
  • 同时将当前 r e m a i n N o d e s remainNodes remainNodes 计数减去出度为 1 1 1 的节点数目,将最外层的度为 1 1 1 的叶子节点取出,并将与之相邻的节点的度减少,重复上述步骤将当前节点中度为 1 1 1 的节点压入队列中;
  • 重复上述步骤,直到剩余的节点数组 r e m a i n N o d e s ≤ 2 remainNodes\le2 remainNodes2 时,此时剩余的节点即为当前高度最小树的根节点。
代码
class Solution {
    public List<Integer> findMinHeightTrees(int n, int[][] edges) {
        List<Integer> ans = new ArrayList<Integer>();
        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<Integer>();
        }
        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<Integer>();
        for (int i = 0; i < n; i++) {                       //先将节点度数为1的节点入队
            if (degree[i] == 1) {
                queue.offer(i);
            }
        }
        int remainNodes = n;                                //剩余节点数为n
        while (remainNodes > 2) {
            int sz = queue.size();                          //队列的大小即为节点度数为1的节点个数
            remainNodes -= sz;
            for (int i = 0; i < sz; i++) {
                int curr = queue.poll();
                for (int v : adj[curr]) {                   //删除队列中节点度数为1的节点,其邻接点度数减1
                    degree[v]--;
                    if (degree[v] == 1) {
                        queue.offer(v);
                    }
                }
            }
        }
        while (!queue.isEmpty()) {                          //剩下的1到2个节点就是答案
            ans.add(queue.poll());
        }
        return ans;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值