【动态规划】打家劫舍

打家劫舍

力扣上打家劫舍相关的题目如下:

序号 题目 区别
1 198. 打家劫舍 不能偷窃相邻的房间
2 213. 打家劫舍 II 房间连成环
3 337. 打家劫舍 III 房间组成一棵二叉树

应用

应用1:Leetcode.198

题目

198. 打家劫舍

分析

\(dp[i]\) 表示前 \(i\) 个房间能获取的最大金额,设房间数量为 \(n\),那么, \(0 \le i \le n - 1\)

边界条件

当只有一个房间时,显然最大收益,就是选择偷窃该房间所所得的收益,当只有两个房间时,收益就是两个房间的最大值,因此边界条件为:

\[\begin{aligned} dp[0] &= nums[0] \\ dp[1] &= \max(nums[0], \ nums[1]) \end{aligned} \]

状态转移

\(i \ge 2\) 时,对于第 \(i\) 个房间\(nums[i]\),有两种选择:

  • 不选择偷窃这个房间,那么,可以获取的最大金额就是 \(dp[i - 1]\)

  • 选择偷窃这个房间,那么,可以获取的最大金额就是 \(dp[i - 2] + nums[i]\);

综上,能获取的最大金额就是上述两种情况的最大值,即状态转移方程:

\[dp[i] = \max(dp[i - 1], \ dp[i - 2] + nums[i]), \quad i \ge 2 \]

代码实现

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if (n == 1) {
            return nums[0];
        }

        int[] dp = new int [n];
        dp[0] = nums[0];
        dp[1] = Math.max(nums[1], nums[0]);
        for (int i = 2; i < n; i++) {
            dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
        }
        return dp[n - 1];
    }
}

对其压缩状态,优化后的实现:

class Solution:
    def rob(self, nums: List[int]) -> int:
        n = len(nums)
        if n == 1:
            return nums[0]
        a = nums[0]
        b = max(nums[1], nums[0])
        c = b
        for i in range(2, n):
            c = max(b, a + nums[i])
            a = b
            b = c
        return c

应用2:Leetcode.213

题目

213. 打家劫舍 II

分析

\(dp[i]\) 表示前 \(i\) 个房间能获取的最大金额,设房间数量为 \(n\)

题目中的限制条件,可以等价于:

  • 若偷窃了第一个房间,就不能偷窃最后一个房间,即可以偷窃房间的范围为:\(nums[0], \ \cdots, \ nums[n -2]\)

  • 若没有偷窃第一个房间,就可以偷窃最后一个房间,即可以偷窃房间的范围为:\(nums[1], \ \cdots, \ nums[n -1]\)

因此,我们只需要针对上述两种情况,利用前面一道题的分析结果,分别计算两种情况的最大值,最后,再选择最优解即可。

代码实现

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if (n == 1) {
            return nums[0];
        }

        int[] nums1 = Arrays.copyOfRange(nums, 0, n - 1);
        int[] nums2 = Arrays.copyOfRange(nums, 1, n);
        return Math.max(doRob(nums1), doRob(nums2));
    }

    private int doRob(int[] nums) {
        int n = nums.length;
        if (n == 1) {
            return nums[0];
        }

        int [] dp = new int [n];
        dp[0] = nums[0];
        dp[1] = Math.max(nums[1], nums[0]);
        boolean canUseLastRoom = n % 2 == 0;
        for (int i = 2; i < n; i++) {
            dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
        }
        return dp[n - 1];
    }
}

应用3:Leetcode.337

题目

337. 打家劫舍 III

分析

由于,我们一般采用递归的方式遍历一棵树,所以,求解树上的动态规划时,通常采用自顶向下的方式,通过分解子问题的方式求解。

对于这道题,我们注意到对于任意一个节点,它都会有两种状态:选择当前节点或者不选择节点。

因此,在遍历二叉树的过程中,为了表示两种状态,我们定一个递归函数:int [] doRob(TreeNode root) ,它会返回一个数组 \([profit_1, profit_2]\),表示当前节点 \(node\) 可以获取的收益,其中数组的值 \(profit[0]\)\(profit[1]\) 分别表示选择该节点的最大收益不选择该节点的最大收益

对于任意一个节点 \(node\),我们都有两种选择:选择当前节点或者不选择节点,因此:

  • 选择当前节点,那么,此时的收益就是:当前节点的值,加上不选择两个子节点对应的收益;

  • 不选当前节点,那么,此时的收益就是:两个子节点的收益之和。

    其中,每一个子节点的最大收益等于,选择该子节点的收益或者不选择该子节点的收益的最大值;

这里,我们选择通过后序遍历的方式计算每个节点收益,即在离开当前节点的时候,计算当前节点的收益。

代码实现

class Solution {
    public int rob(TreeNode root) {
        int [] profits = doRob(root);
        return Math.max(profits[0], profits[1]);
    }

    private int [] doRob(TreeNode root) {
        if (root == null) {
            return new int[2];
        }
        // 左子树的收益
        int [] left = doRob(root.left);
        // 右子树的收益
        int [] right = doRob(root.right);

        // 选择当前节点:此时,左右子树都能不能选择
        int profit1 = root.val + left[1] + right[1];
        // 不选择当前节点:此时,左右子树各自取最大值即可
        int profit2 = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
        return new int [] {profit1, profit2};
    }
}
posted @ 2023-01-16 21:55  LARRY1024  阅读(29)  评论(0编辑  收藏  举报