动态规划 | 打家劫舍1、2、3

198. 打家劫舍

动态规划 | 打家劫舍1、2、3_第1张图片
https://leetcode.cn/problems/house-robber/description/

dp[i] 表示 考虑到下标为 i (包括i)的房子,可以偷到的最大金额。

dp[i] 有两个状态,分别是 偷 和 不偷。
偷,则需要考虑前 i-2 天的最大金额 + nums[1]。
不偷,则考虑 i-1 天的最大金额即可。
那么递推公式应为:dp[i] = max(dp[i-2] + nums[i], dp[i-1])

dp[0] = nums[0], dp[1] = max(nums[0], nums[1])

class Solution {
    public int rob(int[] nums) {
        if (nums.length == 1) {
            return nums[0];
        }
        int[] dp = new int[nums.length];
        
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0], nums[1]);
        for (int i = 2; i < nums.length; i++) {
            dp[i] = Math.max(dp[i-2] + nums[i], dp[i-1]);
            // System.out.println(Arrays.toString(dp));
        }
        return dp[nums.length - 1];
    }
}

213. 打家劫舍 II

动态规划 | 打家劫舍1、2、3_第2张图片
房子首尾相连,只有三种情况,
第一,首尾均不偷
第二,考虑偷首,尾不能投
第三,首不能投,考虑偷尾
第二种和第三种情况包含第一种情况。 两种情况其实就是上一题的思路。

class Solution {
    public int rob(int[] nums) {
        if (nums == null || nums.length == 0) return 0;
        if (nums.length == 1) return nums[0];
        // 首尾相连,只需要考虑两种情况:考虑包含首元素和考虑包含尾元素
        int res1 = robHelper(nums, 0, nums.length - 1);
        int res2 = robHelper(nums, 1, nums.length);
        
        return Math.max(res1, res2);
    }

    private int robHelper(int[] nums, int start, int end) {

        if (start == end - 1) return nums[start];

        int[] dp = new int[nums.length];
        dp[start] = nums[start];
        dp[start + 1] = Math.max(nums[start], nums[start + 1]);

        for (int i = start + 2; i < end; i++) {
            dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
            // System.out.println(Arrays.toString(dp));
        }
        
        return dp[end - 1];
    }
}

337. 打家劫舍 III

动态规划 | 打家劫舍1、2、3_第3张图片

暴力递归 and 记忆化递归:

每个节点有两种状态,偷和不偷
偷,则不能偷左右子节点
不偷,则可以考虑偷左右子节点,注意是考虑,也有可能不偷。

Map<TreeNode, Integer> map = new HashMap<>();
public int rob2(TreeNode root) {
    // 记忆化递归, 1ms
    if (root == null) return 0;
    if (root.left == null && root.right == null) return root.val;
    if (map.containsKey(root)) return map.get(root);

    // 偷父节点
    int val1 = root.val;
    if (root.left != null) val1 += rob(root.left.left) + rob(root.left.right);
    if (root.right != null) val1 += rob(root.right.left) + rob(root.right.right);

    // 不偷父节点
    int val2 = rob(root.left) + rob(root.right);
    map.put(root, Math.max(val1, val2));
    return Math.max(val1, val2);
}

动态规划:
这道题目算是树形dp的入门题目,因为是在树上进行状态转移,我们在讲解二叉树的时候说过递归三部曲,那么下面我以递归三部曲为框架,其中融合动规五部曲的内容来进行讲解。

  1. 确定递归函数的参数和返回值:

    其实这里的返回数组就是dp数组。

    所以dp数组(dp table)以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。所以本题dp数组就是一个长度为2的数组!
    而且,递归的每一层都会保存当前节点的 dp 数组

  2. 确定终止条件
    即,当参数 root 为 null 时,返回[0, 0] 数组

  3. 确定遍历顺序
    一定是后续遍历,因为需要递归返回值,来做下一步计算。
    通过递归左节点,得到左节点偷与不偷的金钱。
    通过递归右节点,得到右节点偷与不偷的金钱。

  4. 确定单层递归的逻辑
    如果是偷当前节点,那么左右孩子就不能偷,val1 = root.val + left[0] + right[0]; (如果对下标含义不理解就再回顾一下dp数组的含义)
    如果不偷当前节点,那么考虑偷左右孩子,即取其中最大的即可 val2 = max(left[0], left[1]) + max(right[0], right[1])
    最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}

  5. 举例推导dp数组

class Solution {

    public int rob(TreeNode root) {
        int[] res = robTree(root);
        return Math.max(res[0], res[1]);
    }

    private int[] robTree(TreeNode root) {
        if (root == null) return new int[]{0, 0};
        // 后续遍历
        int[] left = robTree(root.left);
        int[] right = robTree(root.right);
        
        // 偷当前节点,那么就不能偷左右节点。
        int val1 = root.val + left[0] + right[0];
        // 不偷当前节点,那么就考虑偷左右节点,可偷可不偷,即取较大的情况
        int val2 = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
        return new int[]{val2, val1};
    }
}

你可能感兴趣的:(动态规划,算法)