题目介绍
力扣337题:leetcode-cn.com/problems/ho…
解法一、暴力递归 - 最优子结构
在解法一和解法二中,我们使用爷爷、两个孩子、4 个孙子来说明问题 首先来定义这个问题的状态,爷爷节点获取到最大的偷取的钱数呢
首先要明确相邻的节点不能偷,也就是爷爷选择偷,儿子就不能偷了,但是孙子可以偷,二叉树只有左右两个孩子,一个爷爷最多 2 个儿子,4 个孙子
根据以上条件,我们可以得出单个节点的钱该怎么算?
4 个孙子偷的钱 + 爷爷的钱 VS 两个儿子偷的钱 哪个组合钱多,就当做当前节点能偷的最大钱数。这就是动态规划里面的最优子结构
由于是二叉树,这里可以选择计算所有子节点
-
4 个孙子投的钱加上爷爷的钱如下
int method1 = root.val + rob(root.left.left) + rob(root.left.right) + rob(root.right.left) + rob(root.right.right)
-
两个儿子偷的钱如下
int method2 = rob(root.left) + rob(root.right);
-
挑选一个钱数多的方案则
int result = Math.max(method1, method2);
将上述方案写成代码如下
public int rob(TreeNode root) {
if (root == null) {
return 0;
}
int money = root.val;
if (root.left != null) {
//当前节点加上左边孙子节点的钱
money += (rob(root.left.left) + rob(root.left.right));
}
if (root.right != null) {
//当前节点加上右边孙子节点的钱
money += (rob(root.right.left) + rob(root.right.right));
}
return Math.max(money, rob(root.left) + rob(root.right));
}
但是这种方式提交会出现超时。
解法二、记忆化 - 解决重复子问题
针对解法一种速度太慢的问题,经过分析其实现,我们发现爷爷在计算自己能偷多少钱的时候,同时计算了 4 个孙子能偷多少钱,也计算了 2 个儿子能偷多少钱。这样在儿子当爷爷时,就会产生重复计算一遍孙子节点。
于是乎我们发现了一个动态规划的关键优化点
重复子问题
我们这一步针对重复子问题进行优化,我们在做斐波那契数列时,使用的优化方案是记忆化,但是之前的问题都是使用数组解决的,把每次计算的结果都存起来,下次如果再来计算,就从缓存中取,不再计算了,这样就保证每个数字只计算一次。
由于二叉树不适合拿数组当缓存,我们这次使用哈希表来存储结果,TreeNode 当做 key,能偷的钱当做 value
解法一加上记忆化优化后代码如下:
public int rob(TreeNode root) {
HashMap<TreeNode, Integer> memo = new HashMap<>();
return robInternal(root, memo);
}
public int robInternal(TreeNode root, HashMap<TreeNode, Integer> memo) {
if (root == null) {
return 0;
}
if (memo.containsKey(root)) {
return memo.get(root);
}
int money = root.val;
if (root.left != null) {
money += (robInternal(root.left.left, memo) + robInternal(root.left.right, memo));
}
if (root.right != null) {
money += (robInternal(root.right.left, memo) + robInternal(root.right.right, memo));
}
int result = Math.max(money, robInternal(root.left, memo) + robInternal(root.right, memo));
memo.put(root, result);
return result;
}
说到这里,肯定会有人对上面的最优子结构有疑问,我选择一个儿子和另外两个孙子不行吗
因为另外两个孙子偷钱的情况已经包含在另外一个儿子中、所以儿子能偷到的最大值肯定大于两个孙子,所以这种情况可以去掉了