【树形 DP】树形 DP 的通用思路

题目描述

这是 LeetCode 上的 310. 最小高度树 ,难度为 中等

Tag : 「树形 DP」、「DFS」、「动态规划」

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

给你一棵包含 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 个节点的树,标记为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 到 <math xmlns="http://www.w3.org/1998/Math/MathML"> n − 1 n - 1 </math>n−1 。给定数字 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 和一个有 <math xmlns="http://www.w3.org/1998/Math/MathML"> n − 1 n - 1 </math>n−1 条无向边的 edges 列表(每一个边都是一对标签),其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> e d g e s [ i ] = [ a i , b i ] edges[i] = [a_i, b_i] </math>edges[i]=[ai,bi] 表示树中节点 <math xmlns="http://www.w3.org/1998/Math/MathML"> a i a_i </math>ai 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> b i b_i </math>bi 之间存在一条无向边。

可选择树中任何一个节点作为根。当选择节点 <math xmlns="http://www.w3.org/1998/Math/MathML"> x x </math>x 作为根节点时,设结果树的高度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> h h </math>h 。在所有可能的树中,具有最小高度的树(即,min(h))被称为 最小高度树 。

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

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

示例 1:

lua 复制代码
输入:n = 4, edges = [[1,0],[1,2],[1,3]]

输出:[1]

解释:如图所示,当根是标签为 1 的节点时,树的高度是 1 ,这是唯一的最小高度树。

示例 2:

css 复制代码
输入:n = 6, edges = [[3,0],[3,1],[3,2],[3,4],[5,4]]

输出:[3,4]

提示:

  • <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 < = n < = 2 × 1 0 4 1 <= n <= 2 \times 10^4 </math>1<=n<=2×104
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> e d g e s . l e n g t h = n − 1 edges.length = n - 1 </math>edges.length=n−1
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 < = a i , b i < n 0 <= ai, bi < n </math>0<=ai,bi<n
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> a i ! = b i ai != bi </math>ai!=bi
  • 所有 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( a i , b i ) (ai, bi) </math>(ai,bi) 互不相同
  • 给定的输入保证是一棵树,并且不会有重复的边

树形 DP

这是一道树形 DP 模板题。

当确定以某个点为根节点时,整棵树的形态唯一固定,不妨以编号为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 的节点作为根节点进行分析。

假设当前处理到的节点为 u,其是从父节点 fa 遍历而来,且将要遍历的子节点为 j

即树的形态如图所示(一些可能有的出边用虚线表示):

树形 DP 问题通常将问题根据「方向」进行划分。

对于当前处理到的节点 u 而言,我们根据是否考虑「从 fau 的出边」将其分为「往上」和「往下」两个方向。

假设我们可以通过 DFS 预处理出 <math xmlns="http://www.w3.org/1998/Math/MathML"> f f </math>f 数组和 <math xmlns="http://www.w3.org/1998/Math/MathML"> g g </math>g 数组:

  • <math xmlns="http://www.w3.org/1998/Math/MathML"> f [ u ] f[u] </math>f[u] 代表在以 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 号点为根节点的树中,以 u 节点为子树根节点时,往下的最大高度
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> g [ u ] g[u] </math>g[u] 代表在以 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 号点为根节点的树中,以 u 节点为子节点时,往上的最大高度

那么最终以 u 为根节点的最大高度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> max ⁡ ( f [ u ] , g [ u ] ) \max(f[u], g[u]) </math>max(f[u],g[u])。

<math xmlns="http://www.w3.org/1998/Math/MathML"> f [ u ] f[u] </math>f[u] 只需要简单的 DFS 即可处理出来。对于 <math xmlns="http://www.w3.org/1998/Math/MathML"> g [ u ] g[u] </math>g[u] 而言,其同样包含「往上」和「往下」两部分:

  • 对于经过 fa 后接着往上的部分有 <math xmlns="http://www.w3.org/1998/Math/MathML"> g [ f a ] + 1 g[fa] + 1 </math>g[fa]+1
  • 对于经过 fa 后转而往下的部分,我们需要考虑「fa 节点往下的最大值 <math xmlns="http://www.w3.org/1998/Math/MathML"> f [ f a ] f[fa] </math>f[fa]」是否由 u 节点参与而来进行分情况讨论:
    • 如果 <math xmlns="http://www.w3.org/1998/Math/MathML"> f [ f a ] f[fa] </math>f[fa] 本身不由 u 参与,那么 <math xmlns="http://www.w3.org/1998/Math/MathML"> g [ u ] g[u] </math>g[u] 应当是 fa 节点往下的最大值 <math xmlns="http://www.w3.org/1998/Math/MathML"> + 1 +1 </math>+1 而来( <math xmlns="http://www.w3.org/1998/Math/MathML"> + 1 +1 </math>+1 代表加上 fau 的边)
    • 如果本身 fa 往下的最大值由 u 节点参与,此时应当使用 fa 往下的次大值 <math xmlns="http://www.w3.org/1998/Math/MathML"> + 1 +1 </math>+1 来更新 <math xmlns="http://www.w3.org/1998/Math/MathML"> g [ u ] g[u] </math>g[u]

因此我们需要对 <math xmlns="http://www.w3.org/1998/Math/MathML"> f f </math>f 数组进行拆分,拆分为记录「最大值的 <math xmlns="http://www.w3.org/1998/Math/MathML"> f 1 f1 </math>f1 数组」和记录「次大值的 <math xmlns="http://www.w3.org/1998/Math/MathML"> f 2 f2 </math>f2 数组(注意这里的次大值是非严格的次大值)」,同时使用 <math xmlns="http://www.w3.org/1998/Math/MathML"> p p </math>p 数组记录下取得 <math xmlns="http://www.w3.org/1998/Math/MathML"> f 1 [ u ] f1[u] </math>f1[u] 时 u 的子节点 j 为何值。

实现上,在处理「往上」方向的 DFS 时,为避免对 fa 节点为空的处理,我们可以将「用 fa 来更新 u」调整为「用 u 来更新 j」。

代码:

Java 复制代码
class Solution {
    int N = 20010, M = N * 2, idx = 0;
    int[] he = new int[N], e = new int[M], ne = new int[M];
    int[] f1 = new int[N], f2 = new int[N], g = new int[N], p = new int[N];
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    public List<Integer> findMinHeightTrees(int n, int[][] edges) {
        Arrays.fill(he, -1);
        for (int[] e : edges) {
            int a = e[0], b = e[1];
            add(a, b); add(b, a);
        }
        dfs1(0, -1);
        dfs2(0, -1);
        List<Integer> ans = new ArrayList<>();
        int min = n;
        for (int i = 0; i < n; i++) {
            int cur = Math.max(f1[i], g[i]);
            if (cur < min) {
                min = cur;
                ans.clear();
                ans.add(i);
            } else if (cur == min) {
                ans.add(i);
            }
        }
        return ans;
    }
    int dfs1(int u, int fa) {
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (j == fa) continue;
            int sub = dfs1(j, u) + 1;
            if (sub > f1[u]) {
                f2[u] = f1[u];
                f1[u] = sub;
                p[u] = j;
            } else if (sub > f2[u]) {
                f2[u] = sub;
            }
        }
        return f1[u];
    }
    void dfs2(int u, int fa) {
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (j == fa) continue;
            if (p[u] != j) g[j] = Math.max(g[j], f1[u] + 1);
            else g[j] = Math.max(g[j], f2[u] + 1);
            g[j] = Math.max(g[j], g[u] + 1);
            dfs2(j, u);
        }
    }
}
  • 时间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n)
  • 空间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n)

补充

可能会初次接触「树形 DP」的同学不太理解,这里再补充说明一下。

归根结底,以 u 为根节点的最大深度,必然是下面三种情况之一(往下、往上 和 往上再往下)。

其中对 <math xmlns="http://www.w3.org/1998/Math/MathML"> f f </math>f 数组的拆分(变为 <math xmlns="http://www.w3.org/1998/Math/MathML"> f 1 f1 </math>f1 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> f 2 f2 </math>f2)以及记录取得 <math xmlns="http://www.w3.org/1998/Math/MathML"> f 1 f1 </math>f1 对应的子节点 <math xmlns="http://www.w3.org/1998/Math/MathML"> p [ i ] p[i] </math>p[i],目的都是为了能够正确统计「往上再往下」的情况(统计该情况时,不能考虑从 fa 经过 u 的路径,因此需要记录一个非严格的次大值 <math xmlns="http://www.w3.org/1998/Math/MathML"> f 2 f2 </math>f2)。

最后

这是我们「刷穿 LeetCode」系列文章的第 No.310 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour...

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

更多更全更热门的「笔试/面试」相关资料可访问排版精美的 合集新基地 🎉🎉

相关推荐
算法歌者13 分钟前
[算法]入门1.矩阵转置
算法
喵叔哟20 分钟前
重构代码之取消临时字段
java·前端·重构
林开落L27 分钟前
前缀和算法习题篇(上)
c++·算法·leetcode
远望清一色28 分钟前
基于MATLAB边缘检测博文
开发语言·算法·matlab
tyler_download30 分钟前
手撸 chatgpt 大模型:简述 LLM 的架构,算法和训练流程
算法·chatgpt
SoraLuna1 小时前
「Mac玩转仓颉内测版7」入门篇7 - Cangjie控制结构(下)
算法·macos·动态规划·cangjie
我狠狠地刷刷刷刷刷1 小时前
中文分词模拟器
开发语言·python·算法
鸽鸽程序猿1 小时前
【算法】【优选算法】前缀和(上)
java·算法·前缀和
九圣残炎1 小时前
【从零开始的LeetCode-算法】2559. 统计范围内的元音字符串数
java·算法·leetcode
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts