树形dp总结

树形dp

树形dp通常使用的dfs实现,关键:树形dp将它的子树信息整合起来,返回给父节点,并由父节点的信息更新该节点的信息。

树形动态规划

  • 步骤:先把树转为有根树(如果是几个互不连通的树,就加一个虚拟根,它连接所有孤立的树),然后在树上做DFS,递归到最底层的叶子节点,再一层层返回信息更新至根结点。显然,树上的DP所操作的就是这一层层返回的信息。不同的题目需要灵活设计不同的DP状态和转移方程。
    • 树的存储:邻接表
    • 定义状态
    • 状态转移方程

二叉树的直径【LC543】

给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。

  • 思路

    枚举每个节点往左儿子走的最长链和往右儿子走的最长链,这两条链可能会组成最长直径,然后将左右儿子的最长链返回给父节点

  • 实现

    /**
     * Definition for a binary tree node.
     * public class TreeNode {
     *     int val;
     *     TreeNode left;
     *     TreeNode right;
     *     TreeNode() {}
     *     TreeNode(int val) { this.val = val; }
     *     TreeNode(int val, TreeNode left, TreeNode right) {
     *         this.val = val;
     *         this.left = left;
     *         this.right = right;
     *     }
     * }
     */
    class Solution {
        private int res;
        public int diameterOfBinaryTree(TreeNode root) {
            res = 0;
            dfs(root);
            return res;
        }
        public int dfs(TreeNode root){
            if (root == null) return 0;
            int left = dfs(root.left);
            int right = dfs(root.right);
            res = Math.max(res, left + right);
            return Math.max(left + 1, right + 1);
        }
    }
    
    • 复杂度
      • 时间复杂度: O ( n ) O(n) O(n)
      • 空间复杂度: O ( h e i g h t ) O(height) O(height)

树的直径【LC1245】

给你这棵「无向树」,请你测算并返回它的「直径」:这棵树上最长简单路径的 边数

我们用一个由所有「边」组成的数组 edges 来表示一棵无向树,其中 edges[i] = [u, v] 表示节点 uv 之间的双向边。

树上的节点都已经用 {0, 1, ..., edges.length} 中的数做了标记,每个节点上的标记都是独一无二的。

  • 思路:

    思路同LC543,不同的是树的存储变为邻接表,并且树不一定是二叉树,因此需要存储每个节点向某个方向走的最大的两个值,记录在数组中,那么树的直径可能为该两个值之和,最后同样将某个节点向下走的最大长度返回给根节点。

  • 实现

    class Solution {
        private List<Integer>[] g;
        private int ans;
        private int[] d1;
        private int[] d2;
        public int treeDiameter(int[][] edges) {
            int n = edges.length;
            g = new ArrayList[n + 1];
            Arrays.setAll(g, e -> new ArrayList<>());
            ans = 0;
            d1 = new int[n + 1];
            d2 = new int[n + 1];
            for (int[] edge : edges){
                int u = edge[0], v = edge[1];
                g[u].add(v);
                g[v].add(u);
            }
            dfs(0, -1);
            return ans;
        }
        private int dfs(int u, int start){
            int res = 0;
            for (int v : g[u]){ 
                if (v != start){
                    res = dfs(v, u);
                }
                if (d2[u] > d1[u] && d1[u] < res){
                    d1[u] = res;
                }else if(d2[u] <= d1[u] && res > d2[u]){
                    d2[u] = res;
                }
            }
            ans = Math.max(ans, d1[u] + d2[u]);
            return Math.max(d1[u], d2[u]) + 1;
        }
    
    }
    
    • 复杂度
      • 时间复杂度: O ( n ) O(n) O(n)
      • 空间复杂度: O ( n ) O(n) O(n)

最大价值和与最小价值和的差值【LC2538】

给你一个 n 个节点的无向无根图,节点编号为 0n - 1 。给你一个整数 n 和一个长度为 n - 1 的二维整数数组 edges ,其中 edges[i] = [ai, bi] 表示树中节点 aibi 之间有一条边。

每个节点都有一个价值。给你一个整数数组 price ,其中 price[i] 是第 i 个节点的价值。

一条路径的 价值和 是这条路径上所有节点的价值之和。

你可以选择树中任意一个节点作为根节点 root 。选择 root 为根的 开销 是以 root 为起点的所有路径中,价值和 最大的一条路径与最小的一条路径的差值。

请你返回所有节点作为根节点的选择中,最大开销 为多少。

  • 思路:

    • 由于价值都是正数,因此价值和最小的一条路径一定只有一个点。那么,「价值和最大的一条路径与最小的一条路径的差值」等价于「去掉路径的一个端点」,因此问题可以转化为问题转换成去掉一个叶子后的最大路径和(这里的叶子严格来说是度为 1 的点,因为根的度数也可能是 1)。

    • 使用树形dp找到去掉一个叶子后的最大路径和:在树上做DFS,递归到最底层的叶子节点,再一层层返回「当前带叶子的路径和」和「当前不带叶子的路径和」更新至根结点,对于根节点而言,答案的可能性有两种:

      • 前面最大带叶子的路径和 + 当前不带叶子的路径和;
      • 前面最大不带叶子的路径和 + 当前带叶子的路径和;

      然后更新「最大带叶子的路径和」和「最大不带叶子的路径和」以及结果。

  • 实现

    • 使用邻接表存储二叉树
    • 然后使用树形dp将结果一层一层返回至根节点,由于每个节点只遍历一次,因此不需要写成记忆化搜索的形式,当遇到更大的值时,更新结果
    class Solution {
        private List<Integer>[] g;
        private int[] price;
        private long ans;
    
        public long maxOutput(int n, int[][] edges, int[] price) {
            this.price = price;
            g = new ArrayList[n];
            Arrays.setAll(g, e -> new ArrayList<>());
            for (var e : edges) {
                int x = e[0], y = e[1];
                g[x].add(y);
                g[y].add(x); // 建树
            }
            dfs(0, -1);
            return ans;
        }
    
        // 返回带叶子的最大路径和,不带叶子的最大路径和
        private long[] dfs(int x, int fa) {
            long p = price[x], maxS1 = p, maxS2 = 0;
            for (var y : g[x])
                if (y != fa) {
                    var res = dfs(y, x);
                    long s1 = res[0], s2 = res[1];
                    // 前面最大带叶子的路径和 + 当前不带叶子的路径和
                    // 前面最大不带叶子的路径和 + 当前带叶子的路径和
                    ans = Math.max(ans, Math.max(maxS1 + s2, maxS2 + s1));
                    maxS1 = Math.max(maxS1, s1 + p);
                    maxS2 = Math.max(maxS2, s2 + p); // 这里加上 p 是因为 x 必然不是叶子
                }
            return new long[]{maxS1, maxS2};
        }
    }
    
    作者:灵茶山艾府
    链接:https://leetcode.cn/problems/difference-between-maximum-and-minimum-price-sum/solutions/2062782/by-endlesscheng-5l70/
    来源:力扣(LeetCode)
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    
    • 复杂度
      • 时间复杂度: O ( n ) O(n) O(n)
      • 空间复杂度: O ( n ) O(n) O(n)

二叉树中的最大路径和【LC124】

路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给你一个二叉树的根节点 root ,返回其 最大路径和

  • 思路:

    使用树形dp找到的最大路径和:在树上做DFS,递归到最底层的叶子节点,再一层层返回「左子树最大路径和」和「右子树最大路径和」的较大值至父结点,那么包含该节点的最大路径和为其「左子树最大路径和」+「右子树最大路径和」+该节点的值,注意当左右子树最大路径和大于0时,才选择。记录最大值返回即可

  • 实现

    /**
     * Definition for a binary tree node.
     * public class TreeNode {
     *     int val;
     *     TreeNode left;
     *     TreeNode right;
     *     TreeNode() {}
     *     TreeNode(int val) { this.val = val; }
     *     TreeNode(int val, TreeNode left, TreeNode right) {
     *         this.val = val;
     *         this.left = left;
     *         this.right = right;
     *     }
     * }
     */
    class Solution {
        int ans = Integer.MIN_VALUE;
        public int maxPathSum(TreeNode root) {
            dfs(root);
            return ans;
        }
        private int dfs(TreeNode root){
            if (root == null) return 0;
            int leftGain = Math.max(dfs(root.left), 0);
            int rightGain = Math.max(dfs(root.right), 0);
            int pathSum = leftGain + root.val + rightGain;
            ans = Math.max(ans, pathSum);
            return root.val + Math.max(leftGain, rightGain);
        }
    }
    
    • 复杂度
      • 时间复杂度: O ( n ) O(n) O(n),n为节点数目
      • 空间复杂度: O ( n ) O(n) O(n),空间复杂度主要取决于递归调用层数,最大层数等于二叉树的高度,最坏情况下,二叉树的高度等于二叉树中的节点个数。

统计子树中城市之间最大距离【LC1617】

给你 n 个城市,编号为从 1n 。同时给你一个大小为 n-1 的数组 edges ,其中 edges[i] = [ui, vi] 表示城市 uivi 之间有一条双向边。题目保证任意城市之间只有唯一的一条路径。换句话说,所有城市形成了一棵

一棵 子树 是城市的一个子集,且子集中任意城市之间可以通过子集中的其他城市和边到达。两个子树被认为不一样的条件是至少有一个城市在其中一棵子树中存在,但在另一棵子树中不存在。

对于 d1n-1 ,请你找到城市间 最大距离 恰好为 d 的所有子树数目。

请你返回一个大小为 n-1 的数组,其中第 d 个元素(下标从 1 开始)是城市间 最大距离 恰好等于 d 的子树数目。

请注意,两个城市间距离定义为它们之间需要经过的边的数目。

  • 思路

    由于 n ≤ 15 n \le 15 n15,因此可以使用二进制枚举的方法枚举所有的子树。而子树中节点的最大距离,即为子树中两个节点之间的最长路径,也就是该子树的直径,求解子树的直径可以使用DFS或者BFS,先找到树直径的一个端点,然后从该端点出发,找到树的另一个端点,这两个端点之间的路径长度就是树的直径

  • 实现

    • 首先根据数组edges构建出邻接表g
    • 使用二进制数mask表示子树,第i位为1表示节点i在子树中,否则表示节点i不在子树中
    • 然后枚举子树mask
      • 如果mask的二进制表示中只有一个二进制位为1,那么跳过该mask
      • 否则,找到mask的二进制表示中最高位的二进制为1的位置,进行dfs搜索树的直径,如果mask中的节点全部访问过,表示该mask有效,更新结果;否则表示该mask不是合法的子树
    • 最后返回结果即可
    class Solution {
        private List<Integer>[] g;
        private int mask, vis, diameter;
    
        public int[] countSubgraphsForEachDiameter(int n, int[][] edges) {
            g = new ArrayList[n];
            Arrays.setAll(g, e -> new ArrayList<>());
            for (var e : edges) {
                int x = e[0] - 1, y = e[1] - 1; // 编号改为从 0 开始
                g[x].add(y);
                g[y].add(x); // 建树
            }
    
            var ans = new int[n - 1];
            // 二进制枚举
            for (mask = 3; mask < 1 << n; ++mask) {
                if ((mask & (mask - 1)) == 0) continue; // 需要至少两个点
                vis = diameter = 0;
                dfs(Integer.numberOfTrailingZeros(mask)); // 从一个在 mask 中的点开始递归
                if (vis == mask)
                    ++ans[diameter - 1];
            }
            return ans;
        }
    
        // 求树的直径
        private int dfs(int x) {
            vis |= 1 << x; // 标记 x 访问过
            int maxLen = 0;
            for (int y : g[x])
                if ((vis >> y & 1) == 0 && (mask >> y & 1) == 1) { // y 没有访问过且在 mask 中
                    int ml = dfs(y) + 1;
                    diameter = Math.max(diameter, maxLen + ml);
                    maxLen = Math.max(maxLen, ml);
                }
            return maxLen;
        }
    }
    
    作者:灵茶山艾府
    链接:https://leetcode.cn/problems/count-subtrees-with-max-distance-between-cities/solutions/2162612/tu-jie-on3-mei-ju-zhi-jing-duan-dian-che-am2n/
    来源:力扣(LeetCode)
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    
    • 复杂度
      • 时间复杂度: O ( n 2 n ) O(n2^n) O(n2n),枚举子集需要的时间复杂度为 O ( 2 n ) O(2^n) O(2n),求直径需要的时间复杂度为 O ( n ) O(n) O(n)
      • 空间复杂度: O ( n ) O(n) O(n)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值