0
点赞
收藏
分享

微信扫一扫

代码随想录训练营 Day41打卡 动态规划 part08 121. 买卖股票的最佳时机 122. 买卖股票的最佳时机II 123. 买卖股票的最佳时机III

前程有光 2024-08-28 阅读 21

目录

一、做题心得

二、题目与题解

题目一:198.打家劫舍

题目链接

题解:动态规划

题目二:213.打家劫舍II

题目链接

题解:动态规划

 题目三:337.打家劫舍III

题目链接

题解:动态规划

三、小结


一、做题心得

今天是打家劫舍的一天,来到了动态规划章节的Part7。打家劫舍问题是动态规划算法很经典的一个应用,今天将从三道题目对其进行探讨。

话不多说,直接开始今天的内容。

二、题目与题解

题目一:198.打家劫舍

题目链接

198. 打家劫舍 - 力扣(LeetCode)

题解:动态规划

这是打家劫舍问题最经典的应用,我们无法打劫相邻的两个房屋,这里需要注意的是:偷取的房屋之间不一定是只隔一间房屋 -- 这一点容易出错。

对于当前房屋而言,偷与不偷取决于前一个房屋和前两个房屋是否被偷,这就说明前边状态会对当前状态造成影响 -- 这正符合动态规划的思想。

首先就是初始化:这里由于当前房屋偷与不偷受到前两天的影响,故初始化至少需要最初的两个房屋。

状态方程 -- 考虑当前状态偷与不偷:dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])

        1.前者:不偷窃第 i 间房屋,偷窃总金额为前 i−1 间房屋的最高总金额 -- 注意:这并不表示就一定要偷第 i-1 间房屋

        2.后者:偷窃第 i 间房屋,那么就不能偷窃第 i−1 间房屋,偷窃总金额为前 i−2 间房屋的最高总金额与第 i 间房屋的金额之和

代码如下:

class Solution {
public:
    int rob(vector<int>& nums) {
        int n = nums.size();
        if (n == 0)     return 0;
        if (n == 1)     return nums[0];
        vector<int> dp(n, 0);       //dp[i]表示下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i](即0 - i)
        dp[0] = nums[0];            //初始化dp[0]与dp[1]
        dp[1] = max(nums[0], nums[1]);
        for (int i = 2; i < n; i++) {
            dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);    //第i个房间偷还是不偷--取较大值
        }
        return dp[n - 1];       //房间数组下标范围:0 - n-1
    }
};

题目二:213.打家劫舍II

题目链接

213. 打家劫舍 II - 力扣(LeetCode)

题解:动态规划

这道题在上一道题的基础上将房屋形成了一个环--首尾相接的环。

那么整体上思路就是一样的,唯一的不同就是我们必须保证首尾两个房屋最多只能选择一个去偷。

关键:通过自定义函数将围成环的打家劫舍问题转换为不围成环的打家劫舍问题(上一道题),得到这两种情况的结果:

    1.考虑头元素,不考虑尾元素:ans1 (注意:考虑不代表一定会偷

    2.考虑尾元素,不考虑头元素:ans2

再取两者较大值即可。

这里需要注意的是:头尾元素都不取的情况有被以上两种情况的任一情况包含在内

代码如下:

class Solution {
public:
    int rob(vector<int>& nums) {
        int n = nums.size();
        if (n == 0)   return 0;         //先考虑特殊情况--没有房屋或者只有一个房屋
        if (n == 1)   return nums[0];
        int ans1 = robRange(nums, 0, n - 2);       //考虑头元素,不考虑尾元素
        int ans2 = robRange(nums, 1, n - 1);       //考虑尾元素,不考虑头元素
        return max(ans1, ans2);
    }
    /*  198.打家劫舍的逻辑  */
    int robRange(vector<int>& nums, int start, int end) {           //自定义函数--不围成一圈的打家劫舍:start -- end
        int n = nums.size();
        if (end == start)       return nums[start];
        vector<int> dp(n);
        dp[start] = nums[start];
        dp[start + 1] = max(nums[start], nums[start + 1]);
        for (int i = start + 2; i <= end; i++) {
            dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
        }
        return dp[end];
    }
};

 题目三:337.打家劫舍III

题目链接

337. 打家劫舍 III - 力扣(LeetCode)

题解:动态规划

这是动态规划在树的应用--树形dp--树形dp就是在树上进行递归公式的推导

题目思路很清晰,就是将打劫问题转移到了树上边来实现。

一开始,感觉很容易想到层序遍历,但是层数过多的情况,会很难判断,而且复杂度较高,行不通。再考虑递归遍历,我们要得到盗取的最大金额,需要得到有返回值的结果--首先想到后序遍历。通过动态规划实现这个问题,我们需要先得到左右节点的值,才能计算当前节点偷与不偷的结果(因为直接相连的房屋不可都进行盗取),这就引出了左右根的遍历方式--很显然,后序遍历是可行的。

如何用后序遍历实现呢?这里我们直接看灵神的的代码,通过dfs函数,通过后序遍历遍历了整棵树,再得出盗取或者不盗取当前节点能得到的最大金额--注意:这里采用pair存储这两种情况的结果,可以降低时间复杂度。

代码表示:rob:偷当前节点       rob_not:不偷当前节点

l_rob;偷左子结点       r_rob:偷右子节点

代码如下:

class Solution {
public:
    pair<int, int> dfs(TreeNode* cur) {     //pair类型函数:第一个元素表示偷当前节点所能得到的最大金额,第二个元素表示不偷时
        if(cur == nullptr) {       //当前节点为空,那么无论偷不偷这个“空节点”,结果都是0 
            return {0, 0};
        }
        /*   后序遍历--左右根   */
        auto [l_rob, l_not_rob] = dfs(cur -> left);     //左--递归遍历左子树
        auto [r_rob, r_not_rob] = dfs(cur -> right);    //右--递归遍历右子树
        int rob = l_not_rob + r_not_rob + cur -> val;       //偷当前节点时,左右子节点都不能偷
        int rob_not = max(l_not_rob, l_rob) + max(r_not_rob, r_rob);    //不偷当前节点,则可以偷也可以不偷左右子节点,分别取左右子树在两种状态下的较大值相加 -- 注意:在这里左右子树是分开考虑的
        return {rob, rob_not};
    }
    int rob(TreeNode* root) {
        auto [root_rob, root_not_rob] = dfs(root);      //调用dfs函数计算整棵树偷与不偷的最大金额,并返回二者中的较大值
        return max(root_rob, root_not_rob);
    }
};

三、小结

今天的打卡到此也就结束了,感觉树的遍历这一块的内容还存在着一些欠缺,后边继续加油。

举报

相关推荐

0 条评论