LeetCode 337. House Robber III 解题报告

本文针对LeetCode 337号问题“打家劫舍III”进行了深入解析,通过递归和动态规划的方法,详细阐述了解题思路,并提供了一段完整的C++代码实现。

LeetCode 337. 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:

  3
 / \
2   3
 \   \ 
  3   1

Maximum amount of money the thief can rob = 3 + 3 + 1 = 7.

Example 2:

    3
   / \
  4   5
 / \   \ 
1   3   1

Maximum amount of money the thief can rob = 4 + 5 = 9.


限制条件

没有明确给出.


解题思路

我的思路:

对于每一个节点,都有两种情况,偷或不偷。对应这两种情况,可以设置两个变量,分别存储偷与不偷的总金额。自然,必须遍历所有的节点,由于我希望的是能够从叶子节点开始处理,一直往上到根节点,所以采用的是递归的方式,为了方便递归,实现时使用的是一个数组,数组的第一个元素存储偷当前节点时获得的总金额,数组第二个元素存储不偷当前节点时获得的总金额。

在一次递归中,我们需要进行的操作就是更新数组的两个变量并且返回该数组,递归的结束条件自然是当前节点为空,那直接返回数组[0, 0]。

if (!root)
    return vector<int>(2,0);

由于题目说了不能连续偷直接相连的两个节点,因此,偷了当前节点,就不能偷左右子节点,所以很自然地,我们会写出如下的逻辑:

偷当前节点的总金额 = 当前节点的金额 + 不偷左子节点的金额 + 不偷右子节点的金额
不偷当前节点的金额 = 偷左子节点的金额 + 偷右子节点的金额

用代码表示,就会是:

vector<int> sum(2,0);

sum[0] += root->val + left[1] + right[1];
sum[1] += left[0] + right[0];

其中sum, left, right都是数组,存储着当前节点,左子节点,右子节点的两种偷窃情况的金额,第一个元素是偷的情况,第二个元素是不偷的情况。
上面的处理解决了这道题的第一个难点,把节点的相互联系给断开,变成只需考虑当前节点的情况,而不再需要考虑当前节点对下一个节点的影响。

目前我们的实现就会是下面那样:

vector<int> robNode(TreeNode* root) {
    if (!root)
        return vector<int>(2,0);

    vector<int> left = robNode(root->left);
    vector<int> right = robNode(root->right);

    vector<int> sum(2,0);

    sum[0] += root->val + left[1] + right[1];
    sum[1] += left[0] + right[0];

    return sum;
}

然而当提交时会发现是wrong answer。对于

  2
 / \
1   3
 \
  4 

这个测试用例,我们上面的代码返回的是6(偷2和4),而正确的答案应该是7(偷3和4)。那问题出现在哪里?这就是这道题第二个难点,在进行偷窃情况的更新时,我们陷入了一个误区:不偷当前节点时,我们默认去偷左右子节点
上面的例子中,我们不偷2时,我们就去偷了1和3得到金额4,而金额4比偷2跟4得到的金额6小,所以我们返回了6。然而我们不偷2了,为什么一定要去偷左右子节点?如果孙子节点更大,我们应该做的是去偷孙子节点,而不是偷子节点!所以,不偷当前节点时,我们应该是选择左右子节点中金额更大的偷窃选项。比如上例中,不偷2时,左子节点的情况是[1,4],我们选择的应该是4(表示不偷左子节点,但是偷孙子节点,因为它获得的金额更大)。

所以正确的更新代码应该是:

vector<int> sum(2,0);

sum[0] += root->val + left[1] + right[1];
sum[1] += max(left[0], left[1]) + max(right[0], right[1]);

到了这里,这道题就完成了。

看了Discuss,发现大神的代码跟我的一样,虽然我完全没有想到是动态规划(⊙﹏⊙)b。其中某位大神给出了他整个思维过程Step by step tackling of the problem,可以去学一下怎么从原始的想法思考得出更优的解法。


代码

我的代码

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    int rob(TreeNode* root) {
        vector<int> res = robNode(root);

        return max(res[0], res[1]);
    }

    vector<int> robNode(TreeNode* root) {
        if (!root)
            return vector<int>(2,0);

        vector<int> left = robNode(root->left);
        vector<int> right = robNode(root->right);

        vector<int> sum(2,0);

        sum[0] += root->val + left[1] + right[1];
        sum[1] += max(left[0], left[1]) + max(right[0], right[1]);

        return sum;
    }
};

总结

这道题我是碰巧用对了实现方式,不然估计想半天也想不出来,因为动态规划,着实不怎么学过。不过看了大神的思考,以及结合自己做题的过程,还是有一些自己关于动态递归的感悟。
终于填好了今天的坑,周末就是想懒散一些,不过要坚持,不能太放松,继续加油!填坑之旅还很长的路要走~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值