LeetCode:动态规划中的股票问题【来和我一起用Python炒股吧~】

股票问题是动态规划里面非常非常经典的问题了,本文列举了8道经典题目,都给出了详细的分析。可以发现这些题目的思路都是一致的,只是细节不同而已!赶快刷起来~
\quad
PS. 本文是参考代码随想录做的一些笔记,完整版本请戳链接,非常好的教程!

121. 买卖股票的最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。


贪心:左边取最小,右边取最大,得到的差值就是最大利润。

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        res = 0
        low = float("inf")

        for i in range(len(prices)):
            low = min(low, prices[i])
            res = max(res, prices[i] - low)
        
        return res

动态规划

dp数组的含义:

  • dp[i][0]表示第i天持有股票所得最多现金;
  • dp[i][1]表示第i天不持有股票所得最多现金。

注意这里说的是“持有”,“持有”不代表就是当天“买入”!也有可能是昨天就买入了,今天保持持有的状态。

如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来

  • i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]
  • i天买入股票,所得现金就是买入今天的股票后所得现金即:-prices[i]

那么dp[i][0]应该选所得现金最大的,所以dp[i][0] = max(dp[i - 1][0], -prices[i]);


如果第i天不持有股票即dp[i][1], 也可以由两个状态推出来

  • i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
  • i天卖出股票,所得现金就是按照今天股票佳价格卖出后所得现金即:prices[i] + dp[i - 1][0]

同样dp[i][1]取最大的,dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);

由递推公式可以看出其基础都是要从dp[0][0]dp[0][1]推导出来。那么dp[0][0]表示第0天持有股票,此时的持有股票就一定是买入股票了,因为不可能有前一天推出来,所以dp[0][0] -= prices[0];dp[0][1]表示第0天不持有股票,不持有股票那么现金就是0,所以dp[0][1] = 0;

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        # dp[i][0]表示第i天持有股票所得最多现金;
        # dp[i][1]表示第i天不持有股票所得最多现金。
        dp = [[0] * 2 for _ in range(len(prices))]
        dp[0][0] = -prices[0]
        dp[0][1] = 0

        for i in range(1, len(prices)):
            dp[i][0] = max(dp[i - 1][0], -prices[i])
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i])
        
        return dp[len(prices) - 1][1]

从递推公式可以看出,dp[i]只是依赖于dp[i - 1]的状态。那么我们只需要记录 当前天的dp状态和前一天的dp状态就可以了,可以使用滚动数组来节省空间,代码如下:

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        # dp[i][0]表示第i天持有股票所得最多现金;
        # dp[i][1]表示第i天不持有股票所得最多现金。
        dp = [[0] * 2 for _ in range(2)]
        dp[0][0] = -prices[0]
        dp[0][1] = 0

        for i in range(1, len(prices)):
            dp[i % 2][0] = max(dp[(i - 1) % 2][0], -prices[i])
            dp[i % 2][1] = max(dp[(i - 1) % 2][1], dp[(i - 1) % 2][0] + prices[i])
        
        return dp[(len(prices) - 1) % 2][1]

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

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。返回 你能获得的 最大 利润 。


本题和上题的唯一区别本题股票可以买卖多次了。但是需要注意只有一只股票,所以再次购买前要出售掉之前的股票。

动态规划

dp数组的含义:

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

如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来

  • i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金,即:dp[i - 1][0]
  • i天买入股票,所得现金就是昨天不持有股票的所得现金减去今天的股票价格,即:dp[i - 1][1] - prices[i]

在上题中,因为股票全程只能买卖一次,所以如果买入股票,那么第i天持有股票即dp[i][0]一定就是 -prices[i]。而本题,因为一只股票可以买卖多次,所以当第i天买入股票的时候,所持有的现金可能有之前买卖过的利润。那么第i天持有股票即dp[i][0],如果是第i天买入股票,所得现金就是昨天不持有股票的所得现金 减去 今天的股票价格 即:dp[i - 1][1] - prices[i]

在来看看如果第i天不持有股票即dp[i][1]的情况, 依然可以由两个状态推出来

  • i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
  • i天卖出股票,所得现金就是按照今天股票佳价格卖出后所得现金即:prices[i] + dp[i - 1][0]
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        # dp[i][0]表示第i天持有股票所得最多现金;
        # dp[i][1]表示第i天不持有股票所得最多现金。
        dp = [[0] * 2 for _ in range(len(prices))]
        dp[0][0] = -prices[0]
        dp[0][1] = 0

        for i in range(1, len(prices)):
            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[-1][1]

贪心:这道题的最终利润是可以分解的:假如第0天买入,第3天卖出,那么利润为:prices[3] - prices[0]。相当于(prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0])。此时就是把利润分解为每天为单位的维度,而不是从0天到第3天整体去考虑!那么根据prices可以得到每天的利润序列:(prices[i] - prices[i - 1]).....(prices[1] - prices[0])

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        res = 0
        for i in range(1, len(prices)):
            diff = prices[i] - prices[i - 1]
            if diff >= 0:
                res += diff
        
        return res

123. 买卖股票的最佳时机 III【hard】

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。


关键在于至多买卖两次,这意味着可以买卖一次,可以买卖两次,也可以不买卖。

一天一共就有五个状态,

0 没有操作
1 第一次买入(持有)
2 第一次卖出
3 第二次买入(持有)
4 第二次卖出

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

dp[i][1]表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票,可以理解为持有股票。

递推公式:

达到dp[i][1]状态,有两个具体操作:

  • 操作一:第i天买入股票了,那么dp[i][1] = dp[i - 1][0] - prices[i]
  • 操作二:第i天没有操作,而是沿用前一天买入的状态,即:dp[i][1] = dp[i - 1][1]

dp[i][1]选最大的,所以 dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1])

同理dp[i][2]也有两个操作:

  • 操作一:第i天卖出股票了,那么dp[i][2] = dp[i - 1][1] + prices[i]
  • 操作二:第i天没有操作,沿用前一天卖出股票的状态,即:dp[i][2] = dp[i - 1][2]

所以dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2])

同理可推出剩下状态部分:
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])

综上递推公式为:

dp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][1])
dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2])
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])

发现规律了吗!

dp数组初始化:从递推公式可以看出,需要用到dp[0][0],...,dp[0][4],所以这些都要初始化!

  • 第0天没有操作,就是0,即:dp[0][0] = 0;
  • 第0天做第一次买入的操作,dp[0][1] = -prices[0];
  • 第0天做第一次卖出的操作,首先卖出的操作一定是收获利润,整个股票买卖最差情况也就是没有盈利即全程无操作现金为0,从递推公式中可以看出每次是取最大值,那么既然是收获利润如果比0还小了就没有必要收获这个利润了。所以dp[0][2] = 0;
  • 第0天第二次买入操作,第二次买入依赖于第一次卖出的状态,其实相当于第0天第一次买入了,第一次卖出了,然后在买入一次(第二次买入),那么现在手头上没有现金,只要买入,现金就做相应的减少。所以第二次买入操作,初始化为:dp[0][3] = -prices[0];
  • 同理第二次卖出初始化dp[0][4] = 0

初始化代码如下:

dp[0][0] = 0
dp[0][1] = -prices[0]
dp[0][2] = 0
dp[0][3] = -prices[0]
dp[0][4] = 0

注意规律!

分析完毕,代码如下:

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if len(prices) == 0: return 0
        dp = [[0] * 5 for _ in range(len(prices))]

        # 初始化
        dp[0][0] = 0
        dp[0][1] = -prices[0]
        dp[0][2] = 0
        dp[0][3] = -prices[0]
        dp[0][4] = 0

        for i in range(1, len(prices)):
            for j in range(1, 5):
                dp[i][j] = max(dp[i - 1][j - 1] + ((-1) ** (j % 2)) * prices[i], dp[i - 1][j])
        
        return dp[-1][4]

注意:上面的四个递推公式其实有规律在其中的,所以上面的代码这么简洁!发现了这个规律,那么下一题就简单了~

188. 买卖股票的最佳时机 IV【hard】

给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。


这一题没啥好说的了,注意看上一题的分析,其实把上面的题解改一下就行了,代码如下:

class Solution:
    def maxProfit(self, k: int, prices: List[int]) -> int:
        if len(prices) == 0: return 0
        dp = [[0] * (2 * k + 1) for _ in range(len(prices))]

        # 初始化
        for i in range(1, 2 * k, 2):
            dp[0][i] = -prices[0]
        
        for i in range(1, len(prices)):
            for j in range(1, 2 * k + 1):
                dp[i][j] = max(dp[i - 1][j - 1] + ((-1) ** (j % 2)) * prices[i], dp[i - 1][j])
        
        return dp[-1][2 * k]

309. 最佳买卖股票时机含冷冻期

给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

  • 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。


dp[i][j],第i天状态为j,所剩的最多现金为dp[i][j]

本题出现冷冻期之后,状态比较复杂,例如今天买入股票、今天卖出股票、今天是冷冻期,都是不能操作股票的。 具体可以区分出如下四个状态:

  • 状态一:买入股票状态(今天买入股票,或者是之前就买入了股票然后没有操作)
  • 卖出股票状态,这里就有两种卖出股票状态
    • 状态二:两天前就卖出了股票,度过了冷冻期,一直没操作,今天保持卖出股票状态
    • 状态三:今天卖出了股票
  • 状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天!

分别用0,1,2,3表示上面四个状态。

注意这里的每一个状态,例如状态一,是买入股票状态并不是说今天已经就买入股票,而是说保存买入股票的状态即:可能是前几天买入的,之后一直没操作,所以保持买入股票的状态。(还是理解为持有股票比较好~)

递推公式:

达到买入股票状态(状态一)即:dp[i][0],有两个具体操作:

  • 操作一:前一天就是持有股票状态(状态一),dp[i][0] = dp[i - 1][0]
  • 操作二:今天买入了,有两种情况
    • 前一天是冷冻期(状态四),dp[i - 1][3] - prices[i]
    • 前一天是保持卖出股票状态(状态二),dp[i - 1][1] - prices[i]

所以操作二取最大值,即:max(dp[i - 1][3], dp[i - 1][1]) - prices[i]
那么dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]);

达到保持卖出股票状态(状态二)即:dp[i][1],有两个具体操作:

  • 操作一:前一天就是状态二
  • 操作二:前一天是冷冻期(状态四)

那么,dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);

达到今天就卖出股票状态(状态三),即:dp[i][2] ,只有一个操作:

  • 操作一:昨天一定是买入股票状态(状态一),今天卖出

即:dp[i][2] = dp[i - 1][0] + prices[i];

达到冷冻期状态(状态四),即:dp[i][3],只有一个操作:

  • 操作一:昨天卖出了股票(状态三)

那么,dp[i][3] = dp[i - 1][2]

综上,递推公式为:

dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
dp[i][2] = dp[i - 1][0] + prices[i];
dp[i][3] = dp[i - 1][2];

数组初始化

  • 如果是持有股票状态(状态一)那么:dp[0][0] = -prices[0],买入股票所剩现金为负数。
  • 保持卖出股票状态(状态二),第0天没有卖出dp[0][1]初始化为0就行,
  • 今天卖出了股票(状态三),同样dp[0][2]初始化为0,因为最少收益就是0,绝不会是负数。
  • 同理dp[0][3]也初始为0。

分析完毕,代码实现如下:

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if len(prices) == 0: return 0

        dp = [[0] * 4 for _ in range(len(prices))]

        dp[0][0] = -prices[0]

        for i in range(1, len(prices)):
            dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][1], dp[i - 1][3]) - prices[i])
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][3])
            dp[i][2] = dp[i - 1][0] + prices[i]
            dp[i][3] = dp[i - 1][2]
        
        return max(dp[-1][1:])

714. 买卖股票的最佳时机含手续费

给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。


动态规划:相对于122题,本题只需要在计算卖出操作的时候减去手续费就可以了,代码几乎是一样的。
唯一差别在于递推公式部分。

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

如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来

  • i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]
  • i天买入股票,所得现金就是昨天不持有股票的所得现金减去今天的股票价格 即:dp[i - 1][1] - prices[i]

所以:dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);

在来看看如果第i天不持有股票即dp[i][1]的情况, 依然可以由两个状态推出来

  • i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
  • 第i``天卖出股票,所得现金就是按照今天股票价格卖出后所得现金,注意这里需要有手续费了即:dp[i - 1][0] + prices[i] - fee

所以:dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee)

代码实现如下:

class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        if len(prices) == 0: return 0

        dp = [[0] * 2 for _ in range(len(prices))]

        dp[0][0] = -prices[0]

        for i in range(1, len(prices)):
            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] - fee)
        
        return dp[-1][1]

贪心:如果使用贪心策略,就是最低值买,最高值(如果算上手续费还盈利)就卖。

此时无非就是要找到两个点,买入日期,和卖出日期。

  • 买入日期:其实很好想,遇到更低点就记录一下。
  • 卖出日期:没有必要算出准确的卖出日期,只要当前价格大于(最低价格+手续费),就可以收获利润,至于准确的卖出日期,就是连续收获利润区间里的最后一天(并不需要计算是具体哪一天)。

所以我们在做收获利润操作的时候其实有三种情况:

  • 情况一:收获利润的这一天并不是收获利润区间里的最后一天(不是真正的卖出,相当于持有股票),所以后面要继续收获利润。
  • 情况二:前一天是收获利润区间里的最后一天(相当于真正的卖出了),今天要重新记录最小价格了。
  • 情况三:不作操作,保持原有状态(买入,卖出,不买不卖)

代码实现如下:

class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        result = 0
        minPrice = prices[0]  # 记录最小价格

        for i in range(1, len(prices)):
            # 情况二:相当于买入
            if prices[i] < minPrice:
                minPrice = prices[i]
            
            # 情况三:保持原有状态(因为此时买则不便宜,卖则亏本)
            if minPrice <= prices[i]  < minPrice + fee:
                continue
            
            # 计算利润,可能有多次计算利润,最后一次计算利润才是真正意义的卖出
            if prices[i] > minPrice + fee:
                result += prices[i] - minPrice - fee
                minPrice = prices[i] - fee # 情况一,这一步很关键
        
        return result

从代码中可以看出对情况一的操作,因为如果还在收获利润的区间里,表示并不是真正的卖出,而计算利润每次都要减去手续费,所以要让minPrice = prices[i] - fee,这样在明天收获利润的时候,才不会多减一次手续费!怎么理解呢?

如果当前的股票价格prices[i]大于minPrice + fee,那么我们直接卖出股票并且获得prices[i]−buy的收益。但实际上,我们此时卖出股票可能并不是全局最优的(例如下一天股票价格继续上升),因此我们可以提供一个反悔操作,看成当前手上拥有一支买入价格为prices[i]的股票,将 minPrice更新为prices[i] - fee。这样一来,如果下一天股票价格继续上升,我们会获得 prices[i+1]−prices[i] 的收益,加上这一天prices[i] - minPrice - fee的收益,恰好就等于在这一天不进行任何操作,而在下一天卖出股票的收益。

\quad
\quad
\quad

持续更新…

你可能感兴趣的:(#,LeetCode,动态规划,leetcode,算法,股票问题,python)