Day39--动态规划--198. 打家劫舍,213. 打家劫舍 II,337. 打家劫舍 III
Day39–动态规划–198. 打家劫舍,213. 打家劫舍 II,337. 打家劫舍 III
今天的重点在于最后一题:树形动态规划,树形DP。耳目一新,但是总的思路是一样的。
198. 打家劫舍
思路:
- 确定dp[i]含义:考虑下标[0,i]以内的房屋,最多可以偷窃的金额为dp[i]。(不触发警报)
- 递推公式:
- 对于每个房屋,有两种选择:抢劫或不抢劫
- 抢劫:则加上当前房屋金额和前前个房屋的最大金额
- 不抢劫:则取前一个房屋的最大金额
- 所以递推公式:
dp[i] = Math.max(dp[i-2]+nums[i],dp[i-1]);
- 初始化:
dp[0] = nums[0],dp[1] = Math.max(nums[0],nums[1]);
(这里要注意没有nums[1]的情况) - 遍历顺序:从前往后
class Solution {public int rob(int[] nums) {int n = nums.length;// 初始化前注意处理nums[1];if (n == 1) {return nums[0];}int[] dp = new int[n];// 初始化dp[0] = nums[0];dp[1] = Math.max(nums[0], nums[1]);// 动态规划for (int i = 2; i < n; i++) {dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);}return dp[n - 1];}
}
思路:
观察得知,dp[i] 只跟 dp[i-2] 和 dp[i-1]有关,所以可以节省空间,dp数组不用开n个空间,开三个空间就够了。
// 滚动数组
class Solution {public int rob(int[] nums) {int n = nums.length;// 初始化前注意处理nums[1];if (n == 1) {return nums[0];} else if (n == 2) {return Math.max(nums[0], nums[1]);}// 需要三个变量int[] dp = new int[3];// 初始化dp[0] = nums[0];dp[1] = Math.max(nums[0], nums[1]);// 动态规划for (int i = 2; i < n; i++) {dp[2] = Math.max(dp[0] + nums[i], dp[1]);// 数组往前滚动复制(就像斐波那契数一样)dp[0] = dp[1];dp[1] = dp[2];}return dp[2];}
}
踩坑:注意这里,要特别注意n == 2
的情况,是不进循环的,dp[2]没有被赋值,直接返回dp[2]是错误的。要单独处理这个情况。
213. 打家劫舍 II
思路:
情况一:不考虑抢劫最后一个房屋,从0到n-1
情况二:不考虑抢劫第一个房屋,从1到n
取两者之间较大值。其余步骤同上题。
class Solution {public int rob(int[] nums) {int n = nums.length;if (n == 0) {return 0;}if (n == 1) {return nums[0];}// 情况一:不考虑抢劫最后一个房屋,从0到n-1int res1 = robRange(nums, 0, n - 1);// 情况二:不考虑抢劫第一个房屋,从1到nint res2 = robRange(nums, 1, n);return Math.max(res1, res2);}// 198.打家劫舍的核心逻辑// 注意这里的 start 和 end 是左闭右开private int robRange(int[] nums, int start, int end) {int n = end - start;if (n == 1) {return nums[start];} else if (n == 2) {// 使用滚动dp数组的时候,记得要处理n==2的情况,因为会不进循环,返回错误结果return Math.max(nums[start], nums[start + 1]);}// 滚动dp数组,只存储三个状态int[] dp = new int[3];// 初始化dp[0] = nums[start];dp[1] = Math.max(nums[start], nums[start + 1]);// 从start+2开始计算每个位置的最大抢劫金额for (int i = start + 2; i < end; i++) {dp[2] = Math.max(dp[0] + nums[i], dp[1]);// 数组往前滚动复制(就像斐波那契数一样)dp[0] = dp[1];dp[1] = dp[2];}return dp[2];}
}
337. 打家劫舍 III
方法:递归法
思路:
递归法的关键,是要想到用一个map缓存已经计算过的结点。不然会超时。
// 后序遍历(递归法)
class Solution {Map<TreeNode, Integer> map = new HashMap<>();public int rob(TreeNode root) {return postorderTravel(root);}private int postorderTravel(TreeNode node) {if (node == null) {return 0;}// 叶子节点直接返回其值if (node.left == null && node.right == null) {return node.val;}// 如果已经计算过,直接返回缓存结果if (map.containsKey(node)) {return map.get(node);}// 方案一:偷当前节点int val1 = node.val;// 如果左孩子存在,加上左孩子的左右孩子if (node.left != null) {val1 += rob(node.left.left) + rob(node.left.right);}// 如果右孩子存在,加上右孩子的左右孩子if (node.right != null) {val1 += rob(node.right.left) + rob(node.right.right);}// 方案二:不偷当前节点,考虑左右孩子int val2 = rob(node.left) + rob(node.right);// 取两种方案的最大值,并缓存结果int res = Math.max(val1, val2);map.put(node, res);return res;}
}
方法:动态规划
思路:
树形DP。树形贪心。
这里dp只需要记录两个状态,相当于原来的dp[n-1]和dp[n-2],每个dp[i]都是在本层计算,然后刷新返回给上一层的dp[]。
步骤:
- dp数组的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。
- 递归公式:
- 如果是偷当前节点,那么左右孩子就不能偷,
val1 = cur->val + left[0] + right[0];
- 如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:
val2 = max(left[0], left[1]) + max(right[0], right[1]);
- 如果是偷当前节点,那么左右孩子就不能偷,
- 确定终止条件:
if (cur == NULL) return vector<int>{0, 0};
- 遍历顺序:后续遍历
// 动态规划
class Solution {public int rob(TreeNode root) {int[] res = postorderTravel(root);return Math.max(res[0], res[1]);}// 长度为2的数组。dp[0]存放,不偷本节点的情况的最大值,dp[1]存放偷本节点的最大值private int[] postorderTravel(TreeNode node) {if (node == null) {return new int[] { 0, 0 };}// 左右中// 左int[] left = postorderTravel(node.left);// 右int[] right = postorderTravel(node.right);// 中// 方案一:偷当前节点,那么就不能偷左右节点。int val1 = node.val + left[0] + right[0];// 方案二:不偷当前节点,那么考虑左右节点偷与不偷的情况,反正取较大的情况// 注意这里val2是要把左右的情况汇总加起来的int val2 = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);// val2是不偷,放在第一个位置;val1是偷,放在第二个位置。return new int[] { val2, val1 };}
}