DP动态规划–树型DP

这是我参与更文挑战的第 14 天,活动详情查看: 更文挑战

树型 DP

树型 DP,顾名思义,这一类题目会在一棵二叉树,或者多叉树上进行 DP。虽然看起来是一个二叉树问题,但本质上需要用到 DP 的知识才能求解。

例子:二叉树抢劫

题目

在上次打劫完一条街道之后和一圈房屋后,小偷又发现一个新的可行窃的地区。这个地区只有一个入口,我们称为“根”。 除了“根”之外,每栋房子有且只有一个“父”房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。

输入:[3,2,3,null,3,null,1]

输出:7

解释:小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.

分析

我们看到“最高”两字,应该可以想到使用 DP 方法来试一试。首先还是使出我们的绝招,最后一步。

1. 最后一步

假设小偷在抢劫的时候,总是先抢树的子树,那么最后一步肯定就是二叉树的根结点。根结点只有两种可能:

  • 抢根结点
  • 不抢根结点

得到这两种可能之后,我们只需要在这两种情况中取最大值就可以了。

2. 子问题

如果我们进一步展开根结点的 2 种情况,可以发现:

抢根结点,此时不能抢左右两棵子树的根结点;

不抢根结点,此时可以抢左右两棵子树的根结点,也可以不抢两棵子树的根结点。

我们发现,根结点需要得到两个信息:<抢 root 的最大收益,不抢 root 的最大收益>,并且左右子树也是需要这两个信息。

那么我们可以定义一个函数 f(x),来描述最后一步的需求,以及子问题的需求:

f(x) = <抢 x 的最大收益, 不抢 x 的最大收益>

为了后面描述的方便,我们会用到以下缩写表示上述两个维度的信息:

f(x) = <抢,不抢> = <抢 x 的最大收益, 不抢 x 的最大收益>
max(f(x)) = max<f(x).抢,f(x).不抢>

3. 递推关系

到这里,我们可以根据最后一步和子问题写出递推关系:

f(root).抢 = root.val + f(root.left).不抢 + f(root.right).不抢
f(root).不抢 = max(f(root.left)) + max(f(root.right)))

4. f(x) 的表示

首先我们看一下 f(x) 的返回值,由于返回值只有两个。这比较好处理,对于 Python 来说,可以直接返回两个值,而对于 Java 来说,可以直接返回 long[2] 数组。

然后再看一下 f(x) 的表示。我们从底层开始往上抢的时候,应该只有相邻的两层才会有相互的依赖。

相隔更远的层间信息不需要保留,所以 f(x) 函数并不需要一个哈希或者数组来记录 [x] 的信息。相当于直接使用 DFS,而不需要记忆化 DFS。

5. 初始条件与边界

当遇到一棵空树的时候,只需要返回 long[2] = {0, 0} 就可以了,也就是收益为 0。

6. 计算顺序

这里我们采用的是先抢树的子树,因此,顺序上需要使用后序遍历。

完整代码

到这里,我们已经可以写出如下代码了(解析在注释里):

class Solution {
    // 返回值是一个pair
    // ans[0]表示抢当前根结点
    // ans[1]表示不能抢当前结点
    private long[] postOrder(TreeNode root) {
        if (root == null) {
            return new long[] { 0, 0 };
        }
        long[] l = postOrder(root.left);
        long[] r = postOrder(root.right);
        // 如果要抢当前结点
        long get = root.val;
        // 那么两个子结点必然不能抢
        get += l[1] + r[1];
        // 如果不抢当前结点
        long skip = 0;
        // 那么两个子结点可以抢,也可以不抢
        skip += Math.max(l[0], l[1]) + Math.max(r[0], r[1]);
        return new long[] { get, skip };
    }
    public int rob(TreeNode root) {
        long[] ans = postOrder(root);
        return (int) Math.max(ans[0], ans[1]);
    }
}
复制代码

复杂度分析:时间复杂度,本质上这就是一个后序遍历,所以为 O(N)。算上栈占用的空间,空间复杂度为 O(H),其中 H 表示树的高度。

小结

最后我们再总结一下这个题目的考点:

  • DP分析法
  • 后序遍历

此外,在处理这道题的最后返回值时,后序遍历的返回值并不是直接返回了我们想要的答案,而是带上了子树的信息,然后留给根结点把这部分信息做整合。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享