代码随想录算法训练营Day48|198.打家劫舍、213.打家劫舍II、337.打家劫舍 III

198.打家劫舍

题目链接

文章链接

前言

         分析题干可知,当前房屋偷与不偷取决于前一个房屋和前两个房屋是否被偷。因此当前状态和前面状态会有一种依赖关系,这种依赖关系就是动规的递推公式。

思路

        利用动规五部曲进行分析:

1.确定dp数组及其下标的含义:

        dp[i]:考虑下标i以内的房屋,最多可以偷窃的金额为dp[i]。

2.确定递推公式:

        决定dp[i]的因素就是第i房间偷还是不偷。

        如果偷第i间房间,那么dp[i] = dp[i - 2] + nums[i],因为第i - 1间房不用考虑,找到考虑下标i - 2以内的房屋,最多可以偷窃的金额dp[i - 2]再加上第 i 间房间偷到的钱;

        如果不偷第i间房间,那么dp[i] = dp[i - 1],此时考虑第i间房偷到的最大金额与只考虑第 i - 1间房偷到的金额一样。

        再取二者最大值,dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])

3.dp数组初始化:

        从递推公式可以看出循环遍历第条件是i > 2,因此要对dp[0]、dp[1]进行初始化。

        dp[0] 一定是 nums[0],没有其他房屋可以选择,而dp[1] = max(nums[0], nums[1]),dp[1]可以在第一个和第二个房屋之间进行选择;

4.确定遍历顺序:

        dp[i] 是根据dp[i - 2] 和dp[i - 1]推导出来的,那么一定是从前到后遍历。

5.打印dp数组:

        以示例二,输入[2,7,9,3,1]为例。

代码随想录算法训练营Day48|198.打家劫舍、213.打家劫舍II、337.打家劫舍 III_第1张图片

算法实现 

class Solution {
public:
    int rob(vector& nums) {
        if (nums.size() == 0) return 0;
        if (nums.size() == 1) return nums[0];
        vector dp(nums.size());
        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];
    }
};

        本题是打家劫舍类的入门题目,以下几道是本题的变形。

213.打家劫舍II

题目链接

文章链接

前言

         本题与上题的区别在于,本题的数组首尾相接,成环了。、

思路

        对于一个数组,成环的话主要有如下三种情况(直接借用代码随想录中的图):

        情况一:考虑不包含首尾元

代码随想录算法训练营Day48|198.打家劫舍、213.打家劫舍II、337.打家劫舍 III_第2张图片

        情况二:考虑包含首元素,不包含尾元素 

代码随想录算法训练营Day48|198.打家劫舍、213.打家劫舍II、337.打家劫舍 III_第3张图片

        情况三:考虑包含尾元素,不包含首元素 

代码随想录算法训练营Day48|198.打家劫舍、213.打家劫舍II、337.打家劫舍 III_第4张图片

        由于本题中考虑元素,但不一定选择,因此情况一其实包含在情况二或情况三中,所以只需要考虑后两种情况。

算法实现

class Solution {
public:
    int rob(vector& nums) {
        if (nums.size() == 0) return 0;
        if (nums.size() == 1) return nums[0];
        int result1 = robRange(nums, 0, nums.size() - 2); //情况二
        int result2 = robRange(nums, 1, nums.size() - 1); //情况三
        return max(result1, result2);
    }
    int robRange(vector& nums, int start, int end) {
        if (end == start) return nums[start];
        vector dp(nums.size());
        dp[start] = nums[start];
        dp[start + 1] = max(nums[start], nums[start + 1]);
        for (int i = start + 2; i < nums.size(); i++) {
            dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
        }
        return dp[end];
    }
};

337.打家劫舍 III

题目链接

文章链接

前言

         本题把数据结构改成了树类型,对于树的话,首先就要想到遍历方式,前中后序(深度优先搜索)还是层序遍历(广度优先搜索)。

        本题一定是要后序遍历,因为通过递归函数的返回值来做下一步计算

        与198.打家劫舍,213.打家劫舍II一样,关键是要讨论当前节点抢还是不抢。如果抢了当前节点,两个孩子就不能动,如果没抢当前节点,就可以考虑抢左右孩子。

方法一:暴力递归

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    int rob(TreeNode* root) {
        if (root == NULL) return 0;
        if (root->left == NULL && root->right == NULL) return root->val;

        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);
        return max(val1, val2);
    }
};

        当前代码超时,因为有许多重复计算,我们计算了root的四个孙子(左右孩子的孩子)为头结点的子树的情况,又计算了root的左右孩子为头结点的子树的情况,计算左右孩子的时候其实又把孙子计算了一遍。

        利用记忆化递推进行优化实现,使用一个map把计算过的结果保存一下,这样如果计算过孙子了,那么计算孩子的时候可以复用孙子节点的结果。代码如下:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    unordered_map umap; // 记录已经计算过的结果
    int rob(TreeNode* root) {
        if (root == NULL) return 0;
        if (root->left == NULL && root->right == NULL) return root->val;
        if (umap[root]) return umap[root]; // 如果umap里已有记录,则直接返回

        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);
        umap[root] = max(val1, val2);
        return max(val1, val2);
    }
};

方法二:动态规划

        这道题目算是树形dp的入门题目,因为是在树上进行状态转移,下面以递归三部曲为框架,并融合动规五部曲进行分析。

1.确定递归函数的参数和返回值:

        这里我们要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。参数为当前节点。

vector robTree(TreeNode* cur) {

        里的返回数组就是dp数组。所以dp数组以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。

        所以本题dp数组就是一个长度为2的数组!在递归的过程中,系统栈会保存每一层递归的参数。

2.确定终止条件:

        在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回

if (cur == NULL) return vector{0, 0};

        也相当于dp数组的初始化。

3.确定遍历顺序:

        在二叉树的遍历上是后序遍历;

4.确定单层递归的逻辑:

        如果是偷当前节点,那么左右孩子就不能偷,val1 = cur->val + left[0] + right[0]; 

        如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]);

        最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}

5.打印dp数组:

        以示例1为例,dp数组状态如下:

代码随想录算法训练营Day48|198.打家劫舍、213.打家劫舍II、337.打家劫舍 III_第5张图片

        最后头结点就是 取下标0 和 下标1的最大值就是偷得的最大金钱。 

算法实现

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    int rob(TreeNode* root) {
        vector result = robTree(root);
        return max(result[0], result[1]);
    }

    vector robTree(TreeNode* cur) {
        if (cur == NULL) return vector {0, 0};
        vector left = robTree(cur->left);
        vector right = robTree(cur->right);
        // 偷 cur,则不能偷左右节点
        int val1 = cur->val + left[0] + right[0];
        // 不偷 cur, 要考虑是否偷左右节点
        int val2 = max(left[0], left[1]) + max(right[0], right[1]);
        return {val2, val1};
    }
};

总结

        今天的打家劫舍类题目比较新颖,刚接触确实有些不好上手,尤其是第三道二叉树与动规融合的题目,需要反复练习。

你可能感兴趣的:(算法,动态规划)