「这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战」
题目
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
解题思路
递归做法
打不打劫根节点的选择,会影响后续的打劫行为,分为以下两种情况:
- 打劫根节点,则不能打劫左右子节点,但是能打劫左右子节点的四个子树(如果有)。
- 不打劫根节点,则能打劫左子节点和右子节点,收益是打劫左右子树的收益之和。
var helper = function(root) {
if (root == null) return 0;
// 打劫根节点
let roothelper = root.val;
if(root.left) {
roothelper += helper(root.left.left) + helper(root.left.right);
}
if(root.right) {
roothelper += helper(root.right.left) + helper(root.right.right);
}
let rootNohelper = helper(root.left) + helper(root.right);
return Math.max(rootNohelper, roothelper);
};
上面的递归做法是暴力做法,存在很多重复计算的行为,会导致很长的时间消耗。我们计算了 root 的四个孙子子树,又计算了 root 的左右子树,而后者会把 root 的孙子子树重复计算一遍。 我们把计算过的结果存到 map。下次遇到相同的子问题时直接拿过来用,不用做重复的计算。
记忆化递归做法
为了避免上述的重复计算行为,我们可以使用一个map把计算过的结果保存一下,这样如果计算过孙子节点了,那么计算孩子的时候可以复用孙子节点的结果
// 记忆递归
var rob = function(root) {
const record = new Map();
var helper = function(root) {
if (root == null) return 0;
if(record.has(root)) return record.get(root);
// 打劫根节点
let rootRob = root.val;
if(root.left) {
rootRob += helper(root.left.left) + helper(root.left.right);
}
if(root.right) {
rootRob += helper(root.right.left) + helper(root.right.right);
}
let rootNoRob = helper(root.left) + helper(root.right);
let res = Math.max(rootNoRob, rootRob);
record.set(root, res);
return res;
};
return helper(root);
}