Day48 | 198.打家劫舍, 213.打家劫舍II, 337.打家劫舍III(树形DP)

打家劫舍

LeetCode题目:https://leetcode.cn/problems/house-robber/

  打家劫舍问题的初始问题,很明显,当存在i个房子的时候,打家劫舍的最大收益应该是多少?谈到最大收益,可以判断应该需要使用max来进行处理。同时,打家劫舍要求至少间隔一间房屋,所以递推公式需要考虑当前位置的临近位置,和间隔位置两种情况。因此可以看做是max(间隔房间 + 当前房间收益, 临近房间最大收益),共同来决定最大的收益。

  因此,由以上推理,至少需要初始化两个值才能完成上述状态转移,其中最开始位置一定是当前房屋的收益。而存在两个房间的时候应该是收益最大值。

  代码如下:

class Solution {
public:
    int rob(vector<int>& nums) {
        if(nums.size() == 1) return nums[0];
        vector<int> dp(nums.size(), 0);
        dp[0] = nums[0];
        dp[1] = max(nums[0], nums[1]);
        for (int i = 2; i < nums.size(); i++) {         
            dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
        }
        return dp[nums.size() - 1];
    }
};

打家劫舍II

LeetCode题目:https://leetcode.cn/problems/house-robber-ii/

  与问题1不同点在于,当存在环形的时候,开头的元素和末尾的元素是临近关系的。这就意味着会出现三种情况:(1)开头和末尾的元素都不需要。(2)需要开头的元素。(3)需要末尾的元素。其中(1)情况可以在2/3情况中包含计算。

  因此,其实可以抽象看成,如果情况2,那就默认去掉末尾元素进行最大值计算。如果情况3,那就去掉开头元素进行最大值计算。最后再对两者进行比较,即可得到最后的结果。

  最终代码如下:

class Solution {
public:
    int rangeRob(vector<int>& nums, int begin, int end) {
        vector<int> dp(nums.size(), 0);
        dp[begin] = nums[begin];
        dp[begin + 1] = max(nums[begin], nums[begin + 1]);
        for (int i = begin + 2; i < end; i++) {        
            dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
        }
        return dp[end-1];
    }
    int rob(vector<int>& nums) {
        if(nums.size() == 1) return nums[0];
        if(nums.size() == 2) return max(nums[0], nums[1]);
        int robFront = rangeRob(nums, 1, nums.size());
        int robBack = rangeRob(nums, 0, nums.size() - 1);
        return max(robFront, robBack);
    }
};

打家劫舍III

LeetCode题目:https://leetcode.cn/problems/house-robber-iii/

  该问题主要注重于对树形DP的理解,所谓树形DP,需要在二叉树上进行遍历并同时用返回值来保存状态的转移过程。因此核心在于对返回值的处理上,如何使返回值即表示当前节点的状态,又可以保存节点的值。可以参考DAY37 968.监控二叉树。

  同时,要求得最大收益,因此每次遍历后要对遍历结果对比再进行返回,因此递归要采用后序遍历的方法,来进行递归以收集信息。此外,对于每个返回的值,因为打家劫舍要存在间隔,所以应该分为没有取当前节点的值,和取了当前节点的值两个情况来进行处理。因此,每次递归需要返回两个状态值,使用当前和没有使用当前,即val1 = root->val + leftDp[0] + rightDp[0],val2 = max(leftDp[0],leftDp[1]) + max(rightDp[0],rightDp[1]),最后返回(val1,val2)

  如此,每次递归的流程便确定下来。最终代码如下:

class Solution {
public:
    vector<int> robTree (TreeNode* root) {
        if (root == NULL) return {0, 0};
        vector<int> leftDp = robTree(root->left);
        vector<int> rightDp = robTree(root->right);
        int val1 = root->val + leftDp[0] + rightDp[0];
        int val2 = max(leftDp[0], leftDp[1]) + max(rightDp[0], rightDp[1]);
        return {val2, val1};
    }
    int rob(TreeNode* root) {
        vector<int> result = robTree(root);
        
        return max(result[0], result[1]);
    }
};

  除了dp思路以外,也可以使用常规的递归模式进行处理,同时为了优化一些已知点的最大收益不被反复计算,应该设定map进行映射,以便于对节点是否被计算进行查询,如果已经被计算则直接返回值(理解上其实和状态转移差不多)

class Solution {
public:
    unordered_map<TreeNode*, int> robMap;
    int rob(TreeNode* root) {
        if(root == NULL) return 0;
        if(root->left == NULL && root->right == NULL) return root->val;
        if(robMap.find(root) != robMap.end()) return robMap[root];
        /* 计算当前节点 */
        int val1 = root->val;
        if(root->left) val1 += rob(root->left->left) + rob(root->left->right);
        if(root->right) val1 += rob(root->right->left) + rob(root->right->right);
        /* 不计算当前节点 */
        int val2 = rob(root->left) + rob(root->right);
        robMap[root] = max(val1, val2);
        return robMap[root];
    }
};
06-26 19:23