337. 打家劫舍 III

350 阅读3分钟

题目介绍

力扣337题:leetcode-cn.com/problems/ho…

image.png

image.png

解法一、暴力递归 - 最优子结构

在解法一和解法二中,我们使用爷爷、两个孩子、4 个孙子来说明问题 首先来定义这个问题的状态,爷爷节点获取到最大的偷取的钱数呢

首先要明确相邻的节点不能偷,也就是爷爷选择偷,儿子就不能偷了,但是孙子可以偷,二叉树只有左右两个孩子,一个爷爷最多 2 个儿子,4 个孙子

根据以上条件,我们可以得出单个节点的钱该怎么算?

4 个孙子偷的钱 + 爷爷的钱 VS 两个儿子偷的钱 哪个组合钱多,就当做当前节点能偷的最大钱数。这就是动态规划里面的最优子结构

由于是二叉树,这里可以选择计算所有子节点

  • 4 个孙子投的钱加上爷爷的钱如下

    int method1 = root.val + rob(root.left.left) + rob(root.left.right) + rob(root.right.left) + rob(root.right.right)

  • 两个儿子偷的钱如下

    int method2 = rob(root.left) + rob(root.right);

  • 挑选一个钱数多的方案则

    int result = Math.max(method1, method2);

将上述方案写成代码如下

public int rob(TreeNode root) {
    if (root == null) {
        return 0;
    }

    int money = root.val;
    
    if (root.left != null) {
        //当前节点加上左边孙子节点的钱
        money += (rob(root.left.left) + rob(root.left.right));
    }

    if (root.right != null) {
        //当前节点加上右边孙子节点的钱
        money += (rob(root.right.left) + rob(root.right.right));
    }

    return Math.max(money, rob(root.left) + rob(root.right));
}

但是这种方式提交会出现超时。

解法二、记忆化 - 解决重复子问题

针对解法一种速度太慢的问题,经过分析其实现,我们发现爷爷在计算自己能偷多少钱的时候,同时计算了 4 个孙子能偷多少钱,也计算了 2 个儿子能偷多少钱。这样在儿子当爷爷时,就会产生重复计算一遍孙子节点。

于是乎我们发现了一个动态规划的关键优化点

重复子问题

我们这一步针对重复子问题进行优化,我们在做斐波那契数列时,使用的优化方案是记忆化,但是之前的问题都是使用数组解决的,把每次计算的结果都存起来,下次如果再来计算,就从缓存中取,不再计算了,这样就保证每个数字只计算一次。

由于二叉树不适合拿数组当缓存,我们这次使用哈希表来存储结果,TreeNode 当做 key,能偷的钱当做 value

解法一加上记忆化优化后代码如下:

public int rob(TreeNode root) {
    HashMap<TreeNode, Integer> memo = new HashMap<>();
    return robInternal(root, memo);
}

public int robInternal(TreeNode root, HashMap<TreeNode, Integer> memo) {
    if (root == null) {
        return 0;
    }
    
    if (memo.containsKey(root)) {
        return memo.get(root);
    }
    
    int money = root.val;

    if (root.left != null) {
        money += (robInternal(root.left.left, memo) + robInternal(root.left.right, memo));
    }
    if (root.right != null) {
        money += (robInternal(root.right.left, memo) + robInternal(root.right.right, memo));
    }
    int result = Math.max(money, robInternal(root.left, memo) + robInternal(root.right, memo));
    memo.put(root, result);
    return result;
}

说到这里,肯定会有人对上面的最优子结构有疑问,我选择一个儿子和另外两个孙子不行吗

因为另外两个孙子偷钱的情况已经包含在另外一个儿子中、所以儿子能偷到的最大值肯定大于两个孙子,所以这种情况可以去掉了