代码随想录算法训练day41--动态规划之《股票买卖系列1》

代码随想录算法训练

—day41


前言

今天是算法训练的第41天,希望自己能够坚持下来!
今日任务依旧是动态规划,不过是股票买卖系列:
● 121. 买卖股票的最佳时机
● 122.买卖股票的最佳时机II
● 123.买卖股票的最佳时机III


一、121. 买卖股票的最佳时机

题目链接
文章讲解
视频讲解

贪心法

在贪心系列其实已经做过这题了,回顾一下,
因为股票就买卖一次,那么就是取最左最小值,取最右最大值,那么得到的差值就是最大利润。
代码如下:

class Solution {
public:
    //贪心算法
    int maxProfit(vector<int>& prices) {
        int low = INT_MAX;
        int result = 0;

        for (int i = 0; i < prices.size(); i++) {
            low = min(low, prices[i]); //每次判断是否为最小值
            result = max(result, prices[i] - low); //每次判断卖掉股票最大利润
        }

        return result;
    }
};

动态规划法

思路:

  1. dp[i]:dp[i][0] 表示第i天持有股票所得最多现金 ,dp[i][1] 表示第i天不持有股票所得最多现金

  2. 递推公式:如果第i天持有股票即dp[i][0],
    ①当天买的:因为只能买一次,所以是直接扣第i天的价格: -prices[i]
    ②之前买的:就是保持现状,所得现金就是昨天持有股票的所得现金:dp[i-1][0]
    最后取两者最大值dp[i][0] = max(dp[i - 1][0], -prices[i]);
    如果第i天不持有股票dp[i][1],
    ①之前就不持有了:那么就是延续昨天不持有的现金 dp[i-1][1]
    ②当天卖的:那就是昨天持有的现金再加上当天的价格 dp[i-1][0] + prices[i]
    同样dp[i][1]取最大的,dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);

  3. 初始化:因为需要i-1,所以要初始化dp[0][0]和dp[0][1]
    dp[0][0] 第一天持有,也就是第一天买,所以dp[0][0] = -prices[0]
    dp[0][1]第一天不持有,也就是第一天不买,所以就是初始为0.

  4. 遍历顺序:从递推公式可知,从前往后遍历

  5. 举例推导dp数组:
    以示例1,输入:[7,1,5,3,6,4]为例,dp数组状态如下:
    在这里插入图片描述
    dp[5][1]就是最终结果。

为什么不是dp[5][0]呢?

因为本题中不持有股票状态所得金钱一定比持有股票状态得到的多!

代码如下:

class Solution {
public:
    //dp[i][0]:第i天持有股票的最大金额  dp[i][1]:第i天不持有股票的最大金额
    //递推公式:
    //dp[i][0]: 当天持有:-prices[i] (因为只能买卖一次,之前都是现金0) 之前就持有了:dp[i-1][0]
    //所以 dp[i][0] = max(dp[i-1][0], -prices[i]);
    //dp[i][1]: 当天卖掉:dp[i-1][0]+prices[i]  之前卖掉:dp[i-1][1]
    //dp[i][1] = max(dp[i-1][0]+prices[i], dp[i-1][1])
    //初始化:因为需要i-1,所以初始化dp[0][0] = -prices[0], dp[0][1] = 0
    int maxProfit(vector<int>& prices) {
        int len = prices.size();
        if (len == 0) return 0;

        vector<vector<int>>dp(len, vector<int>(2));
        dp[0][0] = -prices[0];
        dp[0][1] = 0;

        for (int i = 1; i < len; i++) {
            dp[i][0] = max(dp[i-1][0], -prices[i]);
            dp[i][1] = max(dp[i-1][0] + prices[i], dp[i-1][1]);
        }

        return dp[len - 1][1];
    }
};

动态规划空间优化

因为递推公式只跟i-1和i有关,所以只需要维护2*2的数组。可以先写出不优化的版本,再将dp[i]改成dp[i%2]会比较好理解。
代码如下:

class Solution {
public:
    //dp[i][0]:第i天持有股票的最大金额  dp[i][1]:第i天不持有股票的最大金额
    //递推公式:
    //dp[i][0]: 当天持有:-prices[i] (因为只能买卖一次,之前都是现金0) 之前就持有了:dp[i-1][0]
    //所以 dp[i][0] = max(dp[i-1][0], -prices[i]);
    //dp[i][1]: 当天卖掉:dp[i-1][0]+prices[i]  之前卖掉:dp[i-1][1]
    //dp[i][1] = max(dp[i-1][0]+prices[i], dp[i-1][1])
    //初始化:因为需要i-1,所以初始化dp[0][0] = -prices[0], dp[0][1] = 0
    int maxProfit(vector<int>& prices) {
        int len = prices.size();
        if (len == 0) return 0;

        vector<vector<int>>dp(2, vector<int>(2)); //因为只跟i-1有关,所以只需要维护2*2的数组
        dp[0][0] = -prices[0];
        dp[0][1] = 0;

        //所有dp[i]都改成dp[i%2]
        for (int i = 1; i < len; i++) {
            dp[i % 2][0] = max(dp[(i - 1) % 2][0], -prices[i]);
            dp[i % 2][1] = max(dp[(i - 1) % 2][0] + prices[i], dp[(i - 1) % 2][1]);
        }

        return dp[(len - 1) % 2][1];
    }
};

二、122.买卖股票的最佳时机II

题目链接
文章讲解
视频讲解

贪心法

思路:
这道题也是贪心系列做过的一道,因为可以买卖多次,那么计算相邻两天的利润,只收集正利润,累加就可以得到最大利润

代码如下:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
         int sum = 0;

        //计算相邻两天的利润,只收集正利润
         for (int i = 1; i < prices.size(); i++) {
            //if (prices[i] - prices[i - 1] > 0) sum += prices[i] - prices[i -1]; 
            sum += (max(prices[i] - prices[i - 1], 0));
         }

         return sum;
    }
};

动态规划法

跟121. 买卖股票的最佳时机一样,可以用dp数组分别表示当天持有和不持有的最大金额。

  1. dp[i]:
    ①dp[i][0] 表示第i天持有股票所得现金。
    ②dp[i][1] 表示第i天不持有股票所得最多现金
  2. 递推公式:如果第i天持有股票即dp[i][0],
    ①当天买的:因为可以多次买卖,所以是前一天不持有再扣第i天的价格: dp[i - 1][1] - prices[i],这里是跟121. 买卖股票的最佳时机一样的唯一区别。
    ②之前买的:就是保持现状,所得现金就是昨天持有股票的所得现金:dp[i - 1][0]
    最后取两者最大值dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
    如果第i天不持有股票dp[i][1],
    ①之前就不持有了:那么就是延续昨天不持有的现金 dp[i - 1][1]
    ②当天卖的:那就是昨天持有的现金再加上当天的价格 dp[i - 1][0] + prices[i]
    同样dp[i][1]取最大的,dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
  3. 初始化:因为需要i-1,所以要初始化dp[0][0]和dp[0][1]
    dp[0][0] 第一天持有,也就是第一天买,所以dp[0][0] = -prices[0]
    dp[0][1]第一天不持有,也就是第一天不买,所以就是初始为0.
  4. 遍历顺序:从递推公式可知,从前往后遍历

代码如下:(滚动数组版本)

class Solution {
public:
    //递推公式:dp[i][0] 因为可以多次买卖,所以
    //之前不持有,当天持有 dp[i-1][1] - prices[i]  之前就持有 dp[i-1][0]
    int maxProfit(vector<int>& prices) {
        int len = prices.size();
        if (len == 0) return 0;

        vector<vector<int>> dp(2, vector<int>(2));
        dp[0][0] = -prices[0];
        dp[0][1] = 0;

        for (int i = 1; i < len; i++) {
            dp[i % 2][0] = max(dp[(i - 1) % 2][0], dp[(i - 1) % 2][1] - prices[i]);
            dp[i % 2][1] = max(dp[(i - 1) % 2][1], dp[(i - 1) % 2][0] + prices[i]);
        }

        return dp[(len-1) % 2][1];
    }
};

三、123.买卖股票的最佳时机III

题目链接
文章讲解
视频讲解

思路:
这道题关键在于最多买卖两次,就是可以买一次,也可以两次,也可以不买卖。
那么一天就有五个状态:
0.没有操作 (其实我们也可以不设置这个状态)
1.第一次持有股票
2.第一次不持有股票
3.第二次持有股票
4.第二次不持有股票

动态规划五部曲:

  1. dp[i][j]中 i表示第i天,j为 [0 - 4] 五个状态,dp[i][j]表示第i天状态j所剩最大现金。

  2. 递推公式:
    ----达到dp[i][1]状态,有两种情况:
    ①当天买入:因为是第一次买入,所以直接-prices[i]
    ②当天没有操作,沿用的前一天的买入状态:dp[i-1][1]
    ----达到dp[i][2]状态,有两种情况:
    ①当天卖出:dp[i-1][1] + prices[i] 前一天持有+当天卖出价格
    ②当天没有操作,沿用的前一天的卖出状态:dp[i-1][1]
    ----达到dp[i][3]状态,有两种情况:
    ①当天买入:因为是第二次买入,所以是dp[i-1][2] - prices[i] 前一天第一次不持有股票-当天价格
    ②当天没有操作,沿用的前一天的买入状态:dp[i-1][3]
    ----达到dp[i][4]状态,有两种情况:
    ①当天卖出:dp[i-1][3] + prices[i] 前一天第二次持有+当天卖出价格
    ②当天没有操作,沿用的前一天的卖出状态:dp[i-1][4]

  3. 初始化:
    第0天没有操作,这个最容易想到,就是0,即:dp[0][0] = 0;
    第0天做第一次买入的操作,dp[0][1] = -prices[0];
    当天买入,当天卖出,所以dp[0][2] = 0;
    可以当成买了又卖,再第二次买入操作,所以dp[0][3] = -prices[0];
    同理,第二次卖出初始化dp[0][4] = 0;

代码如下:

class Solution {
public:
    //dp[i][0]:不操作
    //dp[i][1]:第一次持有
    //dp[i][2]:第一次不持有
    //dp[i][3]:第二次持有
    //dp[i][4]:第二次不持有
    int maxProfit(vector<int>& prices) {
        int len = prices.size();
        if (len == 0) return 0;

        vector<vector<int>>dp (len, vector<int>(5));
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        dp[0][2] = 0;
        dp[0][3] = -prices[0];
        dp[0][4] = 0;

        for (int i = 1; i < len; i++) {
            dp[i][1] = max(dp[i-1][1], -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[len-1][4];
    }
};

总结

股票买卖系列:
1.dp[i][j]:用下标i表示第i天,用下标j表示第i天的状态,每一天都有 持有和不持有的状态
2.需要分清楚总共可以买卖的次数,只能买卖一次和可以多次买卖的区别在于dp[i][0]第i天持有股票,只能买卖一次是直接-prices[i];多次买卖是dp[i-1][1]-prices[i];
3.限定了只能买卖2次的话,把状态分成第一次持有,第一次不持有,第二次持有,第二次不持有。

明天继续加油!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值