动态规划 - 337. 打家劫舍 III

68 阅读1分钟

4月日新计划更文活动 第21天

前言

动态规划专题,从简到难通关动态规划。

每日一题

今天的题目是 337. 打家劫舍 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

题解

递归

这道题给出了一棵二叉树,每个节点代表一个房屋,在相邻的房屋中只能选择偷取其中的一个,问可以获得的最大价值是多少。

针对这个问题,我们可以使用递归求解。具体来说,对于当前节点,定义一个长度为2的数组res,res[0]表示不偷当前节点获得的最大价值,res[1]表示偷当前节点获得的最大价值,然后递归计算当前节点左右子树不偷和偷的最大价值,最后根据当前节点加上子树的值,得到当前节点偷或不偷所获得的最大价值,然后更新res数组。

递归出口是对于空节点返回[0, 0]。最后返回解为根节点res数组中的两个值中的较大值。

在求解当前节点的最大价值时,只有两个选项:偷当前节点或者不偷当前节点,因为如果偷了当前节点,那么左右子树的子节点都不能偷了;如果不偷当前节点,那么左右子树的节点可以根据需要选取,取到数量的最大值。因此,利用这种深度优先遍历的方式一定能够得到正确的最大价值。

/**
 * Definition for a binary tree node.
 * class TreeNode {
 *     val: number
 *     left: TreeNode | null
 *     right: TreeNode | null
 *     constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
 *         this.val = (val===undefined ? 0 : val)
 *         this.left = (left===undefined ? null : left)
 *         this.right = (right===undefined ? null : right)
 *     }
 * }
 */

function rob(root: TreeNode | null): number {
    if (!root) {
        return 0;
    }
    if (!root.left && !root.right) {
        return root.val;
    }
    let val1 = root.val;
    if (root.left) {
        val1 += rob(root.left.left) + rob(root.left.right);
    }
    if (root.right) {
        val1 += rob(root.right.left) + rob(root.right.right);
    }
    const val2 = rob(root.left) + rob(root.right);
    return Math.max(val1, val2);
};

image.png

动态规划

  1. 确定dp数组以及下标的含义

dp[i][0] 表示在 i 节点不选择时所能获得的最大价值;dp[i][1] 表示在 i 节点选择时所能获得的最大价值。

  1. 确定递推公式

因为一个节点只有两种状态,即偷和不偷,所以递推公式很简单易得:

dp[i][0] = max(dp[left][0], dp[left][1]) + max(dp[right][0], dp[right][1]); 

dp[i][1] = dp[left][0] + dp[right][0] + node_val; 

对于任何一个节点,都有两种情况: 偷:那么不能选择当前节点的左右子节点。 不偷:那么可以选择左右子节点的最大值。

  1. dp数组如何初始化

对于 NULl 或叶子节点,其 dp 值均为 [0,0]。

  1. 确定遍历顺序

从叶子节点往根节点的顺序进行计算。

代码:

/**
 * Definition for a binary tree node.
 * class TreeNode {
 *     val: number
 *     left: TreeNode | null
 *     right: TreeNode | null
 *     constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
 *         this.val = (val===undefined ? 0 : val)
 *         this.left = (left===undefined ? null : left)
 *         this.right = (right===undefined ? null : right)
 *     }
 * }
 */

function rob(root: TreeNode | null): number {
   const dfs = (node) => {
        if (!node) return [0, 0];
        const left = dfs(node.left);
        const right = dfs(node.right); 

        const dp = new Array(2).fill(0);
        dp[1] = node.val + left[0] + right[0];
     
        dp[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
        return dp;
    }

    const res = dfs(root);
    return Math.max(res[0], res[1]);
};

image.png