动态规划之树型DP入门题目:打家劫舍3

521 阅读3分钟

题目描述

这是一道力扣上面的题目: leetcode.cn/problems/ho…

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。

视频题解:www.bilibili.com/video/BV1j2…

文字题解

一、定义状态

遇到这种情况,一般都从最简单的情况开始分析。

根据题目,对于下面这种结构的树,对于父节点G而言,我们可以有两种偷取方式:

第一种方式

父节点G加上左右子节点的子节点(孙子节点),即

robmoney1 = sum(节点G + 节点A + 节点B + 节点C + 节点D)

第二种方式

抢左右子节点,即 robmoney2 = sum(节点E + 节点F)

然后计算这两种偷取方式的最大值:max(robmoney1,robmoney2)

二、状态转移

这明显就是有规则的遍历,对于树的题目,涉及到遍历,一般都可以用递归来解决

于是可以写出以下代码:

# Definition for a binary tree node.
class TreeNode(object):
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

class Solution(object):
    def rob(self, root):
        """
        :type root: TreeNode
        :rtype: int
        """
        if not root:
            return 0

        # 第一种方式:偷父节点加上父节点的左右孙子节点 
        robMoney1 = root.val 
        if root.left:
            robMoney1 += self.rob(root.left.left) + self.rob(root.left.right)
        if root.right:
            robMoney1 += self.rob(root.right.left) + self.rob(root.right.right)

        # 第二种方式:偷父节点的子节点 
        robMoney2 = self.rob(root.left)+self.rob(root.right)

        # 取两种方式的最大值
        return max(robMoney1, robMoney2)

递归最大的问题就是有很多重复计算,这是因为在这个过程中,会从根节点开始向下递归,先计算子树对应的状态,再往上计算父节点对应的状态,直到计算完整棵树。

避免这种问题可以把每次偷过相同的节点都记录下来,所以对于下面这种类型的树

记录表格是这样的:

可以加一个哈希表来记录,于是代码又变成了这个样子:

# Definition for a binary tree node.
class TreeNode(object):
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

class Solution(object):
    def __init__(self):
        self.memoMoney = {}

    def rob(self, root):
        """
        :type root: TreeNode
        :rtype: int
        """
        if not root:
            return 0

        # 如果这个节点先前已经计算过,直接用记录的结果,避免重复计算
        if self.memoMoney.get(root):
            return self.memoMoney[root]

        # 第一种方式:偷父节点加上父节点的左右孙子节点 
        robMoney1 = root.val 
        if root.left:
            robMoney1 += self.rob(root.left.left) + self.rob(root.left.right)
        if root.right:
            robMoney1 += self.rob(root.right.left) + self.rob(root.right.right)

        # 第二种方式:偷父节点的子节点 
        robMoney2 = self.rob(root.left)+self.rob(root.right)

        # 取两种方式的最大值
        self.memoMoney[root] = max(robMoney1, robMoney2)
        return self.memoMoney[root]

这种解法的时间复杂度为O(n),空间复杂度为O(n)。

总结

这其实是一道动态规划的题目,树形DP的主要思想,就是将一个大问题分解成若干个小问题进行求解,并将它们的结果合并起来得到最终答案。

在树形DP中,我们通常以树的节点作为子问题的状态,然后采用递归或者记忆化搜索的方式进行求解。

对于这种树形DP的题目,一般步骤如下:

  1. 定义状态:将每个节点定义为一个状态,然后找到合适的状态转移方程式。

  2. 状态转移:通过递归或记忆化搜索的方式进行状态转移,计算出每个节点的最优解。

  3. 合并答案:对于每个子问题的解,通过合适的方式进行合并,得到整个树的最终答案。

树形DP常用的技巧包括:记忆化搜索、递归等等。

补充

如果你不太明白动态规划是什么,可以看下这个题解:

青蛙跳台阶 (题解)

juejin.cn/post/701216…