Leetcode337. House Robber III
The thief has found himself a new place for his thievery again. There is only one entrance to this area, called the “root.” Besides the root, each house has one and only one parent house. After a tour, the smart thief realized that “all houses in this place forms a binary tree”. It will automatically contact the police if two directly-linked houses were broken into on the same night.
Determine the maximum amount of money the thief can rob tonight without alerting the police.
Example 1:
Input: [3,2,3,null,3,null,1]
3
/ \
2 3
\ \
3 1
Output: 7
Explanation: Maximum amount of money the thief can rob = 3 + 3 + 1 = 7.
Example 2:
Input: [3,4,5,1,3,null,1]
3
/ \
4 5
/ \ \
1 3 1
Output: 9
Explanation: Maximum amount of money the thief can rob = 4 + 5 = 9.
这题的解法可以看做动态规划的一步一步优化过程
解法一 暴力递归 最优子结构
最优子结构:通过子问题的最值得到原问题的最值
当前节点(爷爷)能偷的最大钱数 money:
-
爷爷节点偷:2个儿子节点不能偷,4个孙子节点能偷,即1爷爷+4孙子的钱
int money_sob = root.val + rob(root.left.left)+ rob(root.left.right)+ rob(root.right.left)+ rob(root.right.right)
-
爷爷节点不偷:2个儿子节点偷,4个孙子节点不偷,即2儿子的钱
int money_nosob = rob(root.left) + rob(root.right)
-
选二者中的大的:
int money = Math.max(money_sob, money_nosob)
public int rob(TreeNode root) {
if (root == null) return 0;
int val = root.val;
if (root.left != null) {
val += rob(root.left.left) + rob(root.left.right);
}
if (root.right != null) {
val += rob(root.right.left) + rob(root.right.right);
}
return Math.max(val, rob(root.left) + rob(root.right));
}
解法二 记忆化 解决重复子问题
动态规划的两个条件:1. 有最优子结构 2. 重复子问题
解法一的问题出现在:当在爷爷节点计算了2个儿子节点和四个孙子节点,而在下一个递归中到儿子节点我们又重复计算了孙子节点的值。因此我们可以将上一次递归的子孙节点的值存起来,下次递归就不需要重复计算了。所以这题应该用动态规划。
在大多数情况我们使用数组来存储上一次的值,但是数组不适合缓存二叉树,这里使用哈希表来存储节点信息,哈希表的key是Node,哈希表的value是能偷的钱。
- 时间复杂度O(n)
- 空间复杂度O(n)
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 val = root.val;
if (root.left != null) {
val += rob(root.left.left) + rob(root.left.right);
}
if (root.right != null) {
val += rob(root.right.left) + rob(root.right.right);
}
val = Math.max(val, rob(root.left) + rob(root.right));
map.put(root, val);
return val;
}
解法三 树形动态规划
这个解法和Leetcode309. Best Time to Buy and Sell Stock with Cooldown的思路相似
每个节点都有两种状态:偷或者不偷。
定义int[] dp = new int[2]
表示到当前节点结束后(在当前节点偷/不偷)我们今晚一共可以得到的最大钱数,其中dp[0]
表示不偷的状态,dp[1]
代表偷的状态。
-
不偷:今晚到此节点时一共得到的钱数 = 今晚到左孩子后一共得到的最大钱数 + 今晚到右孩子后共得到的最大钱数(取孩子结点两个状态的最大值)。
dp[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
不能写成
dp[0] = left[0] + right[0];
,这种写法默认左孩子偷,右孩子也偷。 -
偷:今晚到此节点时一共得到的钱数 = 当前节点的钱数 + 左孩子状态不偷 + 右孩子状态不偷
dp[1] = root.val + left[0] + right[0]
① 当前节点不偷 = 左节点[max(偷/不偷)] + 右结点[max(偷/不偷)]
② 当前节点偷 = 当前节点钱 + 左节点[不偷] + 右节点[不偷]
public int rob(TreeNode root) {
int[] dp = robSub(root);
return Math.max(dp[0], dp[1]);
}
private int[] robSub(TreeNode root) {
if (root == null) return new int[2];
int[] left = robSub(root.left);
int[] right = robSub(root.right);
int[] dp = new int[2];
dp[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
dp[1] = root.val + left[0] + right[0];
return dp;
}