【LeetCode】 ---- 打家劫舍系列问题思路与题解

这篇博客探讨了LeetCode中的打家劫舍系列问题,包括第198、213和337题。作者详细介绍了如何运用树形动态规划和递归加记忆化的方法来解决这些挑战性的动态规划问题。

198. 打家劫舍

/**
 *  思路: 动态规划
 *  1. 确定dp数组以及下标含义
 *     dp[i]: 考虑下标i(包括i)以内的房屋,最多可以偷窃的金额dp[i]
 *
 *  2. 确定递推公式
 *     dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);
 *              不偷第i号房屋    偷第i号房屋
 *  3. 初始化
 *     dp[0] = nums[0]
 *      dp[1] = Math.max(nums[0], nums[1])
 *
 *  4. 确定遍历顺序
 *     从前往后遍历
 *
 *  5. 举例推导dp数组
 *          2  7  9  3  1
 * dp数组   2  7  11 11 12
    *  时间: O(n)
 *  空间: O(n)
 */
public int rob(int[] nums) {
   if(nums == null || nums.length == 0) {
      return 0;
   }

   if(nums.length == 1) {
      return nums[0];
   }

   int[] dp = new int[nums.length];

   // dp[0]一定是1号房屋的值
   dp[0] = nums[0];
   // 取决于1号和2号房屋的最大值
   dp[1] = Math.max(nums[0], nums[1]);

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

   return dp[nums.length-1];
}

213. 打家劫舍II

/**
 *  思路: 动态规划
 *
 *  成环的偷盗问题分为三种情况:
 *     1. 不考虑首尾元素偷盗
 *      2. 考虑首部元素, 不考虑尾部元素偷盗
 *      3. 考虑尾部元素, 不考虑首部元素偷盗
 *
 *      其中2和3情况已经包括了1, 所以1情况可以忽略
 *      所以最后结果取 Math.max(情况2,情况3)
 *
 *  时间: O(n)
 *  空间: O(n)
 */
public int rob(int[] nums) {
   if(nums == null || nums.length == 0) {
      return 0;
   }

   if(nums.length == 1) {
      return nums[0];
   }

   return Math.max(robRange(nums, 0, nums.length-1), robRange(nums, 1, nums.length));
}

private int robRange(int[] nums, int start, int end) {
   int[] dp = new int[end - start];

   if(dp.length == 1) {
      return nums[start];
   }

   dp[0] = nums[start];
   dp[1] = Math.max(nums[start], nums[start+1]);

   for (int i = 2; i < dp.length; i++) {
      dp[i] = Math.max(dp[i-1], dp[i-2] + nums[start + i]);
   }

   return dp[dp.length-1];
}

337. 打家劫舍III

树形dp

// 题意: 如果偷取了当前节点,那么它的孩子节点都不能偷取; 如果不偷取当前节点,那么它的孩子节点都能偷取

/**
 *  思路: 树形dp
 *
 *  树形dp: 在树上进行递归公式的推导
 * 
 *  对一个节点偷与不偷得到的最大金额进行记录,然后才能用状态转移记录状态的变化
 *
 *  递归函数
 *  1. 确定递归函数的参数和返回值
 *     - 返回值为一个长度为2的数组,表示偷与不偷两个状态
 *     - int[] robTree(TreeNode cur)
 *
 *  2. 确定终止条件
 *     (空节点时,偷与不偷都是为0)
 *     - if(cur == null) {
 *        return new int[]{0, 0};
 *     }
 *
 *  3. 确定遍历顺序
 *     后序遍历(左右根)
 *     通过递归得到左节点,得到左节点偷与不偷的金额
 *      通过递归得到右节点,得到右节点偷与不偷的金额
 *      下标为0表示不偷, 下标为1表示偷
 *      int[] left = robTree(cur.left); // 左
 *      int[] right = robTree(cur.right); // 右
 *
 *  4. 确定单层递归的逻辑
 *     如果偷取当前节点,那么孩子节点都不能偷取, int val1 = cur.val + left[0] + right[0]
 *     如果不偷取当前节点,那么孩子节点都能偷取,偷不偷选一个最大的值, int val2 = Math.max(left[0],left[1]) + Math.max(right[0],right[1])
 *
 *  5. 举例推导
 *        3 {6,7}
 *      /  \
 * {3,2}2   3 {1,3}
 *      \    \
 * {0,3}3    1 {0,1}
 *
 *  时间: O(n)
 *  空间: O(logn)
 */
public int rob(TreeNode root) {
   if(root == null) {
      return 0;
   }
   int[] res = robTree(root);
   return Math.max(res[0], res[1]);
}

private int[] robTree(TreeNode cur) {
   // 递归终止条件
   if(cur == null) {
      return new int[]{0, 0};
   }

   // 后序遍历
   int[] left = robTree(cur.left); // 左
   int[] right = robTree(cur.right); // 右

   // 单层递归逻辑
   // 偷当前节点 + 左节点不偷 + 右节点不偷
   int val1 = cur.val + left[0] + right[0];
   // 不偷取当前节点 + 偷左节点中最大值 + 偷右节点最大值
   int val2 = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);

   return new int[]{val2, val1};
}

递归 + 记忆化

/**
 *  思路: 递归 + 记忆化
 *
 *  实时计算每个节点偷与不偷的情况
 *
 *  时间: O(n)
 *  空间: O(logn)
 */
// 记忆化避免重复计算
Map<TreeNode, Integer> map = new HashMap<>();
public int rob(TreeNode root) {
   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);
}

总结

image-20210613132846741

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值