动态规划(股票问题及打家劫舍系列)

这篇博客详细介绍了LeetCode中一系列关于股票交易和打家劫舍问题的动态规划解决方案,包括只允许买卖一次、多次买卖、最多买卖两次等不同限制条件下的最佳策略。通过实例解析了动态规划的状态转移方程,展示了如何在有限交易次数内最大化收益,以及在考虑手续费和冷冻期等因素时如何调整策略。同时,文章还涵盖了打家劫舍系列问题,探讨了如何在不同条件下选择最佳盗窃路径以获取最大收益。
摘要由CSDN通过智能技术生成

1. 股票问题系列

这张图是股票问题的一个整图,可以通过这个图推导出来所有的股票问题。

  • 对于这个图可能最不好理解的点在于这个k,股票交易的次数,首先买入+卖出才能算作完整的一次交易k,那么在这里规定只有在买入的时候才会消耗这个k
    在这里插入图片描述
    最终的最大收益是dp[n - 1][k][0]而不是dp[n - 1][k][1],因为最后一天卖出肯定比持有收益更高
    在这里插入图片描述
    dp[i-1][0][0] = 0,因为此时并没有操作的次数,所以所能获得的最大利润就是0,所以可以简化方程

1.1 LeetCode第121题—买卖股票的最佳时机(只买一次)

链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/
在这里插入图片描述
就是在股票价格最低的时候选择购入,在股票价格最高的时候选择卖出,然后从中得到最高的利润。

class Solution {
public:
    //dp[i]表示以第i天为结束,所能够获得的最大利润
    //再简单点说就是要在最低点进行买入,在最高点进行售出
    int maxProfit(vector<int>& prices) {
        int minprice = prices[0];
        int Max = 0;
        for(int i = 1;i<prices.size();++i)
        {
            minprice = std::min(minprice,prices[i]);
            Max = std::max(Max,prices[i] - minprice);
        }
        return Max;
    }
};

通用动态规划解法:

class Solution {
public:
    //k是固定的,所以不影响
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        vector<vector<int>> dp(n,vector<int>(2,0));
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for(int i = 1;i<n;++i)
        {
            dp[i][0] = max(dp[i-1][0],dp[i-1][1] + prices[i]);
            dp[i][1] = max(dp[i-1][1],-prices[i]);
        }
        return dp[n-1][0];
    }
};

1.2 LeetCode第122题—买卖股票的最佳时机II(买卖多次)

链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/
在这里插入图片描述
解法1:贪心算法
prices[3] - prices[1] = (prices[3] - prices[2]) + (prices[2] - prices[1]),所以就是要得到所有的正利润,然后相加

class Solution {
public:
    //必须在购买前先将手里的股票卖掉
    //局部最优:收集每天的正利润,全局最优:求得最大利润
    //当然这道题也可以使用动态规划来做
    int maxProfit(vector<int>& prices) {
        int ret = 0;
        //第一天买入第五天卖出,其实这个是可以把他进行拆分的
        for(int i = 1;i<prices.size();++i)
        {
            ret += std::max(prices[i] - prices[i-1],0);
        }
        return ret;
    }
};

解法2:动态规划

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

1.3 LeetCode第122题—买卖股票的最佳时机III(最多两次)

链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/
在这里插入图片描述
对于这道题是不可能忽略k的,对于递推公式,我们依旧可以采用三维的先进性分析,然后在进行降维。
题解思路:
在这里插入图片描述

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if (prices.size() == 0) return 0;
        vector<vector<int>> dp(prices.size(), vector<int>(5, 0));
        dp[0][1] = -prices[0];
        dp[0][3] = -prices[0];
        dp[0][2] = dp[0][4] = 0; //第0天直接第一次卖出和第二次卖出,此时都还没有股票呢,所以初始化为0
        for (int i = 1; i < prices.size(); i++) {
            dp[i][0] = dp[i - 1][0];
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
            dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
            dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
            dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
        }
        return dp[prices.size()-1][4];
    }
};

1.4 LeetCode第714题—买卖股票的最佳时机含手续费(含手续费)

链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/
在这里插入图片描述
在这里插入图片描述

class Solution {
public:
    //我们规定在卖出的时候来扣除这个手续费
    int maxProfit(vector<int>& prices, int fee) {
        int n = prices.size();
        vector<vector<int>> dp(n,vector<int>(2,0));
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for(int i = 1;i<n;++i)
        {
            dp[i][0] = std::max(dp[i-1][0],dp[i-1][1] + prices[i] -fee);
            dp[i][1] = std::max(dp[i-1][1],dp[i-1][0]-prices[i]);
        }
        return dp[n-1][0];
    }
};

1.5 LeetCode第309题—最佳买卖股票时机含冷冻期(含冷冻期)

链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/
在这里插入图片描述
题解思路:此道题目依旧没有对k进行任何的限制,所以在想的时候可以自己动手写一下,但是在推导递推公式的时候,简易把这个k这一维去除掉。对于这道题来说,初始化的条件就变了,因为我们需要知道dp[0][1] ,dp[0][0],dp[1][1],dp[1][0],这几个状态来推导出dp[i-2][0],从这里也可以感觉出来,必须满足递推公式的初始条件
在这里插入图片描述

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        vector<vector<int>> dp(n,vector<int>(2,0));
        //因为只有两个及以上才会有收入,如果只有一个及以下,那么就压根不会有最大金额
        if(n == 1)
            return 0;
        dp[0][1] = -prices[0];
        //仔细思考就会发现,根本不可能i<2,因为如果小于2的话,dp[i-2][0],这个递推公式就进行不下去了
        dp[1][0] = std::max(dp[0][0],dp[0][1] + prices[1]);
        dp[1][1] = std::max(dp[0][1],-prices[1]);
        for(int i = 2;i<n;++i)
        {
            dp[i][0] = std::max(dp[i-1][0],dp[i-1][1] + prices[i]);
            dp[i][1] = std::max(dp[i-1][1],dp[i-2][0]-prices[i]);
        }
        return dp[n-1][0];
    }
};

2. 打家劫舍系列

2.1 LeetCode第198题—打家劫舍

链接:https://leetcode-cn.com/problems/house-robber/
在这里插入图片描述
在这里插入图片描述

class Solution {
public:
    int rob(vector<int>& nums) {
        //如果就没有房屋那么盗取的金额就是0
        //如果只有一家,那么就只能够盗取这一家
        //如果有两家那么就盗取这两家中金额更大的哪一家
        //第n号房屋有两种可能1.它本身不能够被偷窃了,因为他的上一家已经被偷窃过了
        //第二种可能是:它可以偷窃,那么他的上上一家所偷窃的最大价值 + 当前房屋的价值,就是此时第n好房的最大可偷盗价值
        if(nums.empty())
            return 0;
        int n = nums.size();
        //使用一个数组来保存它的每个子过程的解
        vector<int> dp(n+1,0);
        dp[1] = nums[0];
        for(int i = 2;i<=n;++i)
        {
            dp[i] = std::max(dp[i-1],dp[i-2]+nums[i-1]);
        }
        return dp[n];
    }
};

2.2 LeetCode第213题—打家劫舍II(重点)

链接:https://leetcode-cn.com/problems/house-robber-ii/
在这里插入图片描述
这道题即使在做一遍或许也会做不出来,为什么?其实他就是打家劫舍I的一个小变形,要么他就选择偷取第一家,那么最后一家他就不可能偷,要么他就选择不偷第一家,偷最后一家,然后就分为了两种情况,去其中一种偷取较大金额的结果。这个动态规划可以好好的在理解即便,算是一种比较新颖的动态规划。当让了只有在房屋超过三间的时候才需要考虑首尾相连的情况

class Solution {
public:
    //这道题和上一道打家劫舍最大的区别在于首尾是一个相连接的,所以他们就构成了一个圆环
    //[start,end]是所能偷盗的范围
    //其中[0,n-2]表示此时偷第一家了,不能在偷最后一家了
    //[1,n-1]表示不偷第一家,只偷最后一家的范围
    //到现在我觉得都还是没有很清楚的思考到,到底dp[i]表示的是什么
    //dp[i]表示的是从[start,i]所能够偷取的最大金额,那么dp[end]就是我们要的最终答案
    //这道题其实还挺有意思的,考虑的情况是不同的
    int robRange(vector<int>& nums,int start,int end)
    {
        //这里一共开辟n个位置是没有错的
        //这里为啥一定要开辟n个呢?我有点不是太懂
        vector<int> dp(nums.size(),0);
        //从这里也可以明白,dp不是非要从dp[0]开始的,最主要的还是要根据地推的公式不断的推敲结果
        dp[start] = nums[start];
        dp[start+1] = std::max(nums[start],nums[start+1]);
        for(int i = start+2;i<= end;++i)
        {
            dp[i] = std::max(dp[i-1],dp[i-2]+nums[i]);
        }
        return dp[end];
    }
    int rob(vector<int>& nums) {
        //这道题要分为两段进行思考,要么只偷第一家,要么就只偷最后一家,因为这两家你是不可能同时偷的
        int n = nums.size();
        if(n == 0)
            return 0;
        else if(n == 1)
            return nums[0];
        else if(n == 2)
            return std::max(nums[0],nums[1]);
        //从偷第一家和偷最后一家里面选择一种能偷盗的最大金额
        return std::max(robRange(nums,0,n-2),robRange(nums,1,n-1));
    }
};

2.3 LeetCode第337题—打家劫舍III

链接:https://leetcode-cn.com/problems/house-robber-iii/
在这里插入图片描述
其中0表示不偷,1表示偷,但是如果我们采用别的遍历方法就会出现重复的计算问题,但是采用后续的遍历方式就不会出现重复计算的问题,就相当于从最低下一层不断的往上进行计算,都只算了一遍,效率就会高很多。当前节点偷还是不偷所得到的最大金额递推公式是取决于他的下一层的结果的。

class Solution {
public:
    vector<int> helper(TreeNode* root)
    {
        //0表示偷  1表示不偷
        //从递归也能感觉出来,想要得到当前节点所偷盗的最高金额,取决于他的下一层节点
        if(root == nullptr)
            return {0,0};
        vector<int> left = helper(root->left);
        vector<int> right = helper(root->right);
        //不偷
        int val1 = max(left[0],left[1]) + max(right[0],right[1]);
        //偷
        int val2 = root->val + left[0] + right[0];
        return {val1,val2};
    }
    int rob(TreeNode* root) {
        vector<int> result = helper(root);
        return std::max(result[0],result[1]);
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值