NO.198 打家劫舍 简单 、 NO.213 打家劫舍II 中等 、 NO.337 打家劫舍III 中等
NO.198 打家劫舍 简单
思路一:动态规划 dp[i]数组的含义:到第 i 号房子可偷窃的最高金额。
初始化:数组大小[nums.length+1]包含没有房子的情况,dp[0] = 0,dp[1]=nums[0]
即没有房子的时候最高金额 0 ,只有 1 个房子的时候,最高金额就是这个房子中的现金。
状态转移:到第 i 号房子为止偷窃最高金额 = Max(到 i-1 号的最高金额,到 i-2 号的金额 + nums[i-1])
1 2 3 4 5 6 7 8 9 10 public int rob (int [] nums) { if (nums.length == 0 ) return 0 ; int [] dp = new int [nums.length + 1 ]; dp[0 ] = 0 ; dp[1 ] = nums[0 ]; for (int i = 2 ; i < nums.length + 1 ; i++) { dp[i] = Math.max(dp[i - 1 ], dp[i - 2 ] + nums[i - 1 ]); } return dp[nums.length]; }
时间复杂度:O(n) 空间复杂度:O(n)
优化空间 只需要用两个变量 pre 和 cur ,分别记录 dp[i-1] 和 dp[i-2] 即可。
1 2 3 4 5 6 7 8 9 10 public int rob (int [] nums) { if (nums.length == 0 ) return 0 ; int pre = 0 , cur = nums[0 ]; for (int i = 1 ; i < nums.length; i++) { int temp = cur; cur = Math.max(cur, pre + nums[i]); pre = temp; } return cur; }
时间复杂度:O(n) 空间复杂度:O(1)
NO.213 打家劫舍II 中等
思路一:动态规划 较上一题,本题的增加了一个房屋围成一圈的限定,翻译一下就是 1 号房子和 最后一个房子只能选择偷窃一个
,例如输入 [2,3,2] 在上一题中,结果是 4 ,本题的结果是 3,因为首尾房子是相邻的。
回归难点本身,要求我们首尾的房子只能选择一个,那么我们将这个逻辑上环形的偷窃区间,分割成两个线性的偷窃区间:一个含头不含尾[0,n-2]、一个含尾不含头[1,n-1]。
这样问题就转换成了第一题的样子,分别计算两个偷窃区间的最大金额,选择最大的即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public int rob (int [] nums) { if (nums.length == 1 ) return nums[0 ]; return Math.max(helper(nums, 0 , nums.length - 2 ), helper(nums, 1 , nums.length - 1 )); } private int helper (int [] nums, int start, int end) { if (nums.length == 0 ) return 0 ; int pre = 0 , cur = nums[start]; for (int i = start + 1 ; i <= end; i++) { int temp = cur; cur = Math.max(cur, pre + nums[i]); pre = temp; } return cur; }
时间复杂度:O(n) 空间复杂度:O(n) 需要分割成两个线性偷窃区间。
NO.337 打家劫舍III 中等
思路一:深度优先遍历 虽然从数组变成了树,但是问题的本质并没有变化,仍然是隔一个偷窃一次。
仍然是两种情况:1. 不偷根节点,偷左右孩子,分别偷左右孩子的左右孙子。 2. 偷根节点,分别偷根节点的左右孩子的左右孙子节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public int rob (TreeNode root) { if (root == null ) return 0 ; int ans = root.val; if (root.left != null ) { ans += rob(root.left.left) + rob(root.left.right); } if (root.right != null ) { ans += rob(root.right.left) + rob(root.right.right); } return Math.max(ans, rob(root.left) + rob(root.right)); }
上述暴力方法,存在重复大量计算。
记忆化搜索 用一个 Map 保存每个节点和从该节点开始可以偷窃的最大金额的键值对。DFS 过程中先判断当前节点是否已经计算过了,搜搜完成保存当前节点和可偷窃最大值的映射到 Map 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 HashMap<TreeNode, Integer> map = new HashMap<>(); public int rob (TreeNode root) { if (root == null ) return 0 ; if (map.containsKey(root)) return map.get(root); int ans = root.val; if (root.left != null ) { ans += rob(root.left.left) + rob(root.left.right); } if (root.right != null ) { ans += rob(root.right.left) + rob(root.right.right); } ans = Math.max(ans, rob(root.left) + rob(root.right)); map.put(root, ans); return ans; }
计算节点不同状态时的最大金额 这种很像股票买卖问题,我们只需要考虑两种情况:1. 偷根节点。 2. 不偷根节点。 从两种情况中选择可偷窃金额最大的即可。
上面两个方法,比较容易逐步分析得到的,但是这个方法,如果完全没有提示的情况下,还是很难想到的。
用一个数组 ans ,ans[0] 表示偷根节点可偷最大金额, ans[1] 表示不偷根节点可偷最大金额。
递归计算左右子树,因为左右子树的根节点也存在偷和不偷两种情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public int rob (TreeNode root) { int [] ans = dp(root); return Math.max(ans[0 ],ans[1 ]); } private int [] dp(TreeNode root) { int [] ans = new int [2 ]; if (root ==null ) return ans; int [] left = dp(root.left); int [] right = dp(root.right); ans[0 ] = root.val + left[1 ] + right[1 ]; ans[1 ] = Math.max(left[0 ], left[1 ]) + Math.max(right[0 ], right[1 ]); return ans; }
本人菜鸟,有错误请告知,感激不尽!
更多题解和源码:github