310.最小高度树

目标

树是一个无向图,其中任何两个顶点只通过一条路径连接。 换句话说,任何一个没有简单环路的连通图都是一棵树。

给你一棵包含 n 个节点的树,标记为 0 到 n - 1 。给定数字 n 和一个有 n - 1 条无向边的 edges 列表(每一个边都是一对标签),其中 edges[i] = [ai, bi] 表示树中节点 ai 和 bi 之间存在一条无向边。

可选择树中任何一个节点作为根。当选择节点 x 作为根节点时,设结果树的高度为 h 。在所有可能的树中,具有最小高度的树(即,min(h))被称为 最小高度树 。

请你找到所有的 最小高度树 并按 任意顺序 返回它们的根节点标签列表。

树的 高度 是指根节点和叶子节点之间最长向下路径上边的数量。

说明:

  • 1 <= n <= 2 * 10^4
  • edges.length == n - 1
  • 0 <= ai, bi < n
  • ai != bi
  • 所有 (ai, bi) 互不相同
  • 给定的输入 保证 是一棵树,并且 不会有重复的边

思路

一看到这个题目就想起了换根动态规划,参考2581_统计可能的树根数目

这个题是medium,但是感觉比上面参考那个hard题难多了,状态转换方程很难想。基本都是靠错误案例调试出来的。最开始写的dp方法调试了好几个小时,测试通过但是超时。然后开始怀疑dp根本就没法解,因为换根后状态是变化的,需要动态调整高度,并且还要区分当前节点是否为原来的根提供了最大高度。结果改到最后和暴力解法差不多了。

这种解法的关键是弄清楚换根之后节点高度的变化。经过分析只有换根的两个节点受到影响。分为两种情况,如果新根为旧根提供了最大高度,那么旧根应变为其邻接节点次大高度+1(第一次递归进来时计算)。如果新根没有为旧根提供最大高度,旧根高度不变仍为其邻接节点最大高度+1(第一次递归进来时计算)。新根是其邻接节点最大高度+1(这里面包括了刚才改变高度的旧根)。

注意:这里每个节点记录的是以0为根进行dfs,从叶子节点累加的高度。因此,当前节点高度就等于邻接节点最大高度加1。

代码

/**
 * @date 2024-03-17 16:04
 */
public class FindMinHeightTrees {

    public int[] res;
    public int minHeight;
    List<Integer>[] g;
    int[] dp;

    public List<Integer> findMinHeightTrees(int n, int[][] edges) {
        g = new ArrayList[n];
        for (int i = 0; i < g.length; i++) {
            g[i] = new ArrayList<>();
        }
        dp = new int[n];
        res = new int[n];
        minHeight = n;
        for (int i = 0; i < edges.length; i++) {
            g[edges[i][0]].add(edges[i][1]);
            g[edges[i][1]].add(edges[i][0]);
        }
        dfs(0, 0);
        redfs(0, 0);

        List<Integer> minHeights = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            if (res[i] == minHeight) {
                minHeights.add(i);
            }
        }
        return minHeights;

    }

    public void redfs(int root, int parent) {
        // 进入到该层后,保存其最大与次大深度,后面换根后再回来遍历其它兄弟节点时不会受到换根影响
        // 由于是深度遍历,换根到子节点与当前根的深度有关
        // 由子节点返回后,状态已保存,不受换根影响
        int max = 0;
        int second = 0;
        for (Integer neighbor : g[root]) {
            if (dp[neighbor] > max) {
                second = max;
                max = dp[neighbor];
            } else if (dp[neighbor] > second) {
                second = dp[neighbor];
            }
        }
        // max是与root相邻节点的高度,加1才是root的最大高度
        res[root] = max + 1;
        // 更新最小高度
        minHeight = Math.min(minHeight, res[root]);
        for (Integer next : g[root]) {
            if (next == parent) {
                // 遇到叶子节点返回
                continue;
            }
            // 换到下一个根next,修改root的高度,如果下一个点为当前点提供了最大高度,那么当前节点高度为
            // 次高加一,否则是最高加一
            dp[root] = (dp[next] == max ? second : max) + 1;
            redfs(next, root);
        }
    }

    public int dfs(int root, int parent) {
        dp[root] = 1;
        for (Integer next : g[root]) {
            if (next != parent) {
                dp[root] = Math.max(dp[root], dfs(next, root) + 1);
            }
        }
        return dp[root];
    }

    public static void main(String[] args) {
        FindMinHeightTrees main = new FindMinHeightTrees();
//        System.out.println(main.findMinHeightTrees(4, new int[][]{{1, 0}, {1, 2}, {1, 3}}));
//        System.out.println(main.findMinHeightTrees(6, new int[][]{{3, 0}, {3, 1}, {3, 2}, {3, 4}, {5, 4}}));
//        System.out.println(main.findMinHeightTrees(7, new int[][]{{0, 1}, {1, 2}, {1, 3}, {2, 4}, {3, 5}, {4, 6}}));
//        System.out.println(main.findMinHeightTrees(8, new int[][]{{0,1},{1,2},{2,3},{0,4},{4,5},{4,6},{6,7}}));
        System.out.println(main.findMinHeightTrees(11, new int[][]{{0, 1}, {0, 2}, {2, 3}, {0, 4}, {2, 5}, {5, 6}, {3, 7}, {6, 8}, {8, 9}, {9, 10}}));
//        System.out.println(main.findMinHeightTrees(2, new int[][]{{0, 1}}));
    }
}

性能

2368.受限条件下可到达节点的数目

目标

现有一棵由 n 个节点组成的无向树,节点编号从 0 到 n - 1 ,共有 n - 1 条边。

给你一个二维整数数组 edges ,长度为 n - 1 ,其中 edges[i] = [ai, bi] 表示树中节点 ai 和 bi 之间存在一条边。另给你一个整数数组 restricted 表示 受限 节点。

在不访问受限节点的前提下,返回你可以从节点 0 到达的 最多 节点数目。

注意,节点 0 不 会标记为受限节点。

思路

自然的想法是构建图,将受限节点从中删除,然后深度优先遍历,同时记录节点个数。这里构建的图主要是为了获取其连通节点进行dfs,HashSet不太适合。因为数据可能并不是连续存储的,要先计算元素的Hash值,然后从桶中取出链表或者红黑树,才能找到元素。在本例中,性能会下降一倍。

代码

/**
 * @date 2024-03-02 15:39
 */
public class ReachableNodes {
    public int res = 1;
    boolean[] isRestricted;

    public int reachableNodes(int n, int[][] edges, int[] restricted) {
        List<Integer>[] g = new ArrayList[edges.length + 1];
        isRestricted = new boolean[edges.length + 1];
        for (int i : restricted) {
            isRestricted[i] = true;
        }
        for (int i = 0; i < g.length; i++) {
            g[i] = new ArrayList<>();
        }
        for (int[] edge : edges) {
            if (isRestricted[edge[0]] || isRestricted[edge[1]]) {
                continue;
            }
            g[edge[0]].add(edge[1]);
            g[edge[1]].add(edge[0]);
        }
        dfs(0, -1, g);
        return res;
    }

    public void dfs(int root, int parent, List<Integer>[] g) {
        for (Integer n : g[root]) {
            if (n == parent) {
                continue;
            }
            res++;
            dfs(n, root, g);
        }
    }
}

性能

看了官网的答案还可以使用并查集,耗时只要10ms,有时间可以看看。