[leetcode] 310.Minimum Height Trees

For a undirected graph with tree characteristics, we can choose any node as the root. The result graph is then a rooted tree. Among all possible rooted trees, those with minimum height are called minimum height trees (MHTs). Given such a graph, write a function to find all the MHTs and return a list of their root labels.

Format
The graph contains n nodes which are labeled from 0 to n - 1. You will be given the number n and a list of undirected edges (each edge is a pair of labels).

You can assume that no duplicate edges will appear in edges. Since all edges are undirected, [0, 1] is the same as [1, 0] and thus will not appear together in edges.

题目链接:310.Minimum Height Trees

这是一道谷歌公司的题目。我个人觉得这道题目是很具有代表性的。同时也是很难做的。

这道题有许多不同的思路。从这道题的discussion中就可以到许多令人惊艳的想法和思路。我将其总结如下。大致有两种思路。深入的理解和应用这两种思路对提高编程水平是大有裨益的。

首先这道题目要考虑到的是,对于一颗给定的树,我们要寻找的最小高度树(MHT)可能有几种呢?答案是一种或两种。这个结论可以用反证法予以证明。
假设给定一棵树,我们可以找到三种不同根结点的MHT。不失普遍性的情况下,我们可以假设这三个根结点是两两相连接的,记为r1,r2和r3。假设这三者作为根结点所构成的树的高度均为h。那么r1到r3的最大距离为2(或更大,如果这三个结点不是两两相邻的话)。那么r1作为根结点所构成的MHT的高度实际上为h+2(因为r3高为h,而r1距离r3为2以上)。那么r1构成的树就不是MHT,这与我们的假设矛盾。

基于以上的结论。我们知道了实际上我们需要在给定的树中找打1个或2个结点来作为MHT的根结点。

方法1:

若我们由特殊到一般地考虑这道题目,我们首先考虑一个没有任何分支的树,既所谓的路径图(path graph,如1-2-3-4-5)。对于一个有n个结点的路径图,寻找MHT非常简单,只要找到其中间结点即可。假设我们不知道n,也不能任意访问某位置上的结点,那么使用双指针的方法可以方便地寻找其中间结点使用两个指针分别指向该路径图的两端并使它们以相同的速度移动。当它们相遇或者相距一个单位的时候,我们就找到了需要的根结点。

那么对于一颗有分支的树,我们可以使用指针指向所有的叶子结点,然后让这些指针以相同的速度移动,当两个指针相遇的时候,保留一个,然后使其继续移动。只到最后两个指针相遇或者相距一个单位的时候。它们所指的结点即为根结点。
具体实现的时候,我们可以用类似于广度优先拓扑搜索的思路,去掉树的所有叶子结点,更新每个结点的度,然后再去掉所有新产生的叶子结点,只到剩下一个或两个结点为止。
注意对于N个结点的书我们总是有N-1条边。
Java代码如下:
时间和空间复杂度均为O(n)

public List<Integer> findMinHeightTrees(int n, int[][] edges) {
    /*
    find out the leaves in every loop and delete the leaves from the tree. Stop when there is only 1 or 2 nodes left. 
    Since the MHT's root is always at the middle point of the tree, there can at most be 2 different roots for a MHT. 
    */
    if (n == 1) {
        return Collections.singletonList(0);
    } 

    //Use a list of hashset to store the adjacent nodes of every nodes in the tree based on the edges passed in to this program.
    List<Set<Integer>> treeadj = new ArrayList<>(n);
    for (int i = 0; i < n; i++) {
        treeadj.add(new HashSet<>());
    }
    // The following loop can also be wrote as following which is more efficient
    // for (int[] edge : edges) {
    //     treeadj.get(edge[0]).add(edge[1]);
    //     treeadj.get(edge[1]).add(edge[0]);
    // }
    for (int i = 0; i < edges.length; i++) {
        treeadj.get(edges[i][0]).add(edges[i][1]);
        treeadj.get(edges[i][1]).add(edges[i][0]);
    }

    //Use a List leaves to store the leaves of the tree
    //This is the initiation of the leaves List
    List<Integer> leaves = new ArrayList<>();
    for (int i = 0; i < n; i++) {
        if (treeadj.get(i).size() == 1) {
            leaves.add(i);
        }
    }

    while (n > 2) {
        n -= leaves.size();
        //Ust new leaves to store the newly generated leaves becasue of deleting the old leaves from the tree
        List<Integer> newLeaves = new ArrayList<>();
        for (int i = 0; i < leaves.size(); i++) {
            int j = treeadj.get(leaves.get(i)).iterator().next();
            treeadj.get(j).remove(leaves.get(i));
            if (treeadj.get(j).size() == 1) {
                newLeaves.add(j);
            }
        }
        leaves = newLeaves;
    }

    return leaves;
}

方法2:

上面我们已经说明了构成MHT的根结点一定在一棵树距离最远的两个结点构成的路径的中间一个或两个位置。那么我们需要寻找一棵树的最大长度的两个结点的中点。寻找最大长度的方法有两种,一种是树的动态规划(Tree DP),一种是做两次树遍历(BFS或DFS遍历)。下面分别讨论。

  • 做两次树遍历
    首先任意选取树的一个结点x作为遍历开始的根结点,然后利用BFS或DFS找到距离x最远的结点y。y一定位于某一条最大距离的一端。然后以y作为遍历开始的根结点,执行另一次BFS或DFS,找到距离y最远的结点z。那么从y到z的所构成的最远路径的中点就是我们需要寻找的MHT的根。
    Java代码如下:
    时间复杂度和空间复杂度均为O(n)。
public List<Integer> findMinHeightTrees(int n, int[][] edges) {
    /*
    Randomly select a node x as the root, do a dfs/bfs to find the node y that has the longest distance from x.
    Then y must be one of the endpoints on some longest path.
    Let y the new root, and do another dfs/bfs. Find the node z that has the longest distance from y.
    Now, the path from y to z is the longest one, and thus its middle point(s) is the answer.
    */
    if (n <= 0) {
        return new ArrayList<>();
    }

    List<Integer>[] treeAdj = new List[n];
    for (int i = 0; i < n; i++) {
        treeAdj[i] = new ArrayList<>();
    }
    for (int[] edge : edges) {
        treeAdj[edge[0]].add(edge[1]);
        treeAdj[edge[1]].add(edge[0]);
    }
    int[] dist1 = new int[n];
    int[] dist2 = new int[n];
    int[] parent = new int[n];

    bfs(0, treeAdj, dist1, parent, n);

    int root = 0;
    for (int i = 0; i < n; i++) {
        if (dist1[i] > dist1[root]) {
            root = i;
        }
    }

    bfs(root, treeAdj, dist2, parent, n);

    root = 0;
    for (int i = 0; i < n; i++) {
        if (dist2[i] > dist2[root]) {
            root = i;
        }
    }

    List<Integer> list = new ArrayList<>();
    while (root != -1) {
        list.add(root);
        root = parent[root];
    }

    if (list.size() % 2 == 1) return Arrays.asList(list.get(list.size() / 2));
    else return Arrays.asList(list.get(list.size() / 2 - 1), list.get(list.size() / 2));
}

private void bfs(int start, List<Integer>[] adj, int[] dist, int[] parent, int n) {
    Queue<Integer> queue = new ArrayDeque<>();
    boolean[] visited = new boolean[n];
    queue.add(start);
    dist[start] = 0;
    visited[start] = true;
    parent[start] = -1;
    while (!queue.isEmpty()) {
        int u = queue.poll();
        for (int v : adj[u]) {
            if (!visited[v]) {
                visited[v] = true;
                dist[v] = dist[u] + 1;
                parent[v] = u;
                queue.add(v);
            }
        }
    }
}   
  • 动态规划
    利用数组dp[i]储存以i作为根结点时的树的高度。利用DFS和动态规划的思路来计算dp[0]dp[n-1]
    任意选取一个结点作为根结点开始DFS,例如0。
    当我们到达一个结点u的时候,利用T来表示去掉u的子树后余下的树。利用height[]来表示u的高度,使用变量acc记录去掉u的子树后树T中以u作为结束结点的最长路径的长度(既u的深度)。那么显然dp[u] = max{acc, height[u]}。acc对于根结点来说是0。
             |                |
             .                .
            /|\              /|\
           * u *            * u *
            /|\
           / | \
          *  v  *

.表示单独结点,*表示子树。
那么接下来我们需要考虑u的子结点,用v表示。那么v结点的acc显然等于下面两种情况中的最大值
1. acc+1 直接将u点的acc长度向uv延伸1
2. max(height[v’]+2) v’是u的子结点且v’ != v。如下图所示

             u
            /|
           / |
          v' v
          |
          .
          .
          .
          |
          .

情况2看似需要O(k)时间(k为结点u的度),然而实际上我们只需要O(1)时间来处理情况2。方法是我们为每个结点保留其最大高度(height1[])和第二大高度(height2[])两个量(也就是将最大高度的分支去掉后该结点的最大高度)。那么情况2,max{height[v’] + 2}就是: I. height1[u] + 1(当v不在u的最大高度所在的路径上时,也就是height1[u] != height1[v] + 1时),或 II. height2[u] + 1(当v在u的最大高度所在的路径上时,也就是height1[u] == height1[v] + 1时)。
那么经过DFS,所有的节点的最大高度(dp[])都被计算出来之后,我们只需找到高度最小的结点返回即可。
Java代码如下:
时间和空间复杂度均为O(n)。

public List<Integer> findMinHeightTrees(int n, int[][] edges) {

/**
* LeetCode 310 - Minimum Height Trees
*
* Alternatively, one can solve this problem directly by tree dp.
* Let dp[i] be the height of the tree when the tree root is i.
* We compute dp[0] ... dp[n - 1] by tree dp in a dfs manner.
*
* Arbitrarily pick a node, say node 0, as the root, and do a dfs.
* When reach a node u, and let T be the subtree by removing all u's descendant (see the right figure below).
* We maintain a variable acc that keeps track of the length of the longest path in T with one endpoint being u.
* Then dp[u] = max(height[u], acc)
* Note, acc is 0 for the root of the tree.
*
*                 |                 |
*                 .                 .
*                /|\               /|\
*               * u *             * u *
*                /|\
*               / | \
*              *  v  *
*
*  . denotes a single node, and * denotes a subtree (possibly empty).
*
*  Now it remains to calculate the new acc for any of u's child, v.
*  It is easy to see that the new acc is the max of the following
*
*  1) acc + 1 --- extend the previous path by the edge uv;
*  2) max(height[v'] + 2), where v != v' --- see below for an example.
*
*                 u
*                /|
*               / |
*              v' v
*              |
*              .
*              .
*              .
*              |
*              .
*
* In fact, the second case can be computed in O(1) time instead of spending a time proportional to the degree of u.
* Otherwise, the runtime can be quadratic when the degree of some node is Omega(n).
* The trick here is to maintain two heights of each node, the largest height (the conventional height), and the second     largest height
* (the height of the node after removing the branch w.r.t. the largest height).
*
* Therefore, after the dfs, all dp[i]'s are computed, and the problem can be answered trivially.
* The total runtime is still O(n).
*/
    if (n <= 0) return new ArrayList<>();
    if (n == 1) return Arrays.asList(0);

    int[] height1 = new int[n]; //stores the largest height of a node
    int[] height2 = new int[n]; //stores the second largest height of a node
    int[] dp = new int[n];

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

    for (int[] edge : edges) {
        tree[edge[0]].add(edge[1]);
        tree[edge[1]].add(edge[0]);
    }

    dfs(0, -1, height1, height2, tree);
    dfs(0, -1, 0, height1, height2, dp, tree);

    int min = dp[0];
    for (int i = 0; i < n; i++) {
        if (dp[i] < min) {
            min = dp[i];
        }
    }

    List<Integer> result = new ArrayList<>();
    for (int i = 0; i < n; i++) {
        if (dp[i] == min) {
            result.add(i);
        }
    }

    return result;
}

private void dfs(int node, int parent, int[] height1, int[] height2, List<Integer>[] tree) {
    height1[node] = Integer.MIN_VALUE;
    height2[node] = Integer.MIN_VALUE;

    for (int child : tree[node]) {
        if (child != parent) {
            dfs(child, node, height1, height2, tree);
            int tmpheight = height1[child] + 1;
            if (tmpheight > height1[node]) {
                height2[node] = height1[node];
                height1[node] = tmpheight;
            }
            else if(tmpheight > height2[node]) {
                height2[node] = tmpheight;
            }
        }
    }
    height1[node] = Math.max(height1[node], 0);
}

private void dfs(int node, int parent, int acc, int[] height1, int[] height2, int[] dp, List<Integer>[] tree) {
    dp[node] = Math.max(acc, height1[node]);
    for (int child : tree[node]) {
        if (child != parent) {
            int newAcc = Math.max(acc + 1, height1[child] + 1 == height1[node] ? height2[node] + 1 : height1[node] + 1);
            dfs(child, node, newAcc, height1, height2, dp, tree);
        }
    }
}

写完这道题的三种代码。感觉身体被掏空。。。。

这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

耀凯考前突击大师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值