leetcode
leetcode 301 ~ 350
打家劫舍 III

打家劫舍 III

难度:

标签:

题目描述

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

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

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

 

示例 1:

输入: root = [3,2,3,null,3,null,1]
输出: 7 
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7

示例 2:

输入: root = [3,4,5,1,3,null,1]
输出: 9
解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9

 

提示:

  • 树的节点数在 [1, 104] 范围内
  • 0 <= Node.val <= 104

代码结果

运行时间: 26 ms, 内存: 18.0 MB


/*
 * 思路:由于Java Stream不适合处理递归问题,因此我们可以使用辅助方法来简化代码。
 * 我们依然需要两个元素的数组来表示偷与不偷的情况。
 */
 
class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int x) { val = x; }
}
 
import java.util.Arrays;
 
public class Solution {
    public int rob(TreeNode root) {
        return Arrays.stream(robHelper(root)).max().getAsInt();
    }
 
    private int[] robHelper(TreeNode node) {
        if (node == null) {
            return new int[2];
        }
 
        int[] left = robHelper(node.left);
        int[] right = robHelper(node.right);
        return new int[] {
            Math.max(left[0], left[1]) + Math.max(right[0], right[1]),
            node.val + left[0] + right[0]
        };
    }
}
 

解释

方法:

这个题解使用了树形DP的思路。对于每个节点,分别考虑偷或不偷两种情况。如果不偷当前节点,那么可以偷子节点;如果偷当前节点,那么就不能偷子节点。用一个长度为2的数组dp来记录每个节点的这两种情况的最大金钱。其中dp[0]表示不偷该节点的最大金钱,dp[1]表示偷该节点的最大金钱。然后通过后序遍历二叉树,根据左右子节点的dp数组来计算当前节点的dp数组。最后返回根节点的dp数组的最大值即可。

时间复杂度:

O(n)

空间复杂度:

O(h),最坏情况下是 O(n)

代码细节讲解

🦆
在树形DP算法中,递归终止条件为遇到空节点时返回(0, 0),这里为什么可以直接假设空节点的偷和不偷的金额都为0?
在树形DP算法中,当遇到空节点时返回(0, 0)是因为空节点代表该处没有房子可供偷窃,因此不管是选择偷还是不偷,可获得的金额都是0。这样的设置简化了递归的边界条件处理,确保当递归到树的叶子节点的子节点时,计算不会出错,且能正确返回到上一层递归。
🦆
为什么在计算不偷当前节点的金额时,需要比较偷和不偷左右子节点的金额并取最大值?
在计算不偷当前节点的金额时,我们有自由选择偷或不偷其左右子节点以获得最大收益。因此,对每个子节点,我们都需要比较偷和不偷两种情况的收益,并选择其中的最大值。这样做是为了确保即使不偷当前节点,也能从其子节点中获得最大的可能收益。
🦆
在计算偷当前节点的金额时,为什么可以直接加上左右子节点不偷的金额,而不考虑其他情况?
在计算偷当前节点的金额时,根据题目规则,如果偷了当前节点,则不能偷与其直接相连的子节点,以避免触发报警系统。因此,当选择偷取当前节点时,只能考虑左右子节点不偷的情况下的金额。这确保了算法在满足题目条件下尽可能获取最大收益。
🦆
这种后序遍历的方式在处理每个节点时是如何确保不违反题目中的‘如果两个直接相连的房子在同一天晚上被打劫,则房屋将自动报警’的条件的?
后序遍历的方式首先处理所有子节点,然后再处理当前节点。这种遍历顺序保证了在做出是否偷窃当前节点的决定时,已经获得了所有子节点的最优偷窃策略。当选择偷窃当前节点时,会使用子节点不偷的值,确保不违反‘如果两个直接相连的房子在同一天晚上被打劫,则房屋将自动报警’的规则。这样,每一步决策都建立在已解决的子问题的基础上,保证了整个算法的正确性和效率。

相关问题

打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

 

示例 1:

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:

输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
     偷窃到的最高金额 = 2 + 9 + 1 = 12 。

 

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 400

打家劫舍 II

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

 

示例 1:

输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

示例 3:

输入:nums = [1,2,3]
输出:3

 

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 1000