Leetcode 股票买卖系列——动态规划全揽五题


前言

股票买卖问题是一类常见问题,这个系列包括五道题目(仅限于我做过的(+_+)),都是给定某几天的股票价格,要求求出通过交易能获得的最大收益。
每个题都有不同的解决方案,但是同样的动态规划思路可以直接解决这个系列。这篇博文就是介绍这个简单却通吃的动规思路哒~

注:本文参考leetcode上各种题解,大家感兴趣可以去看看,这些题目各自特点不同,因此每道题目都有自己独特的解法,这里不做讨论。本文只是介绍最简单通用的动规方法,且不包括动规的化简(●’◡’●)


一、题目描述

股票系列包括:
121. 买卖股票的最佳时机 简单
122. 买卖股票的最佳时机 II 简单
309. 最佳买卖股票时机含冷冻期 中等
714. 买卖股票的最佳时机含手续费 中等

123. 买卖股票的最佳时机 III 困难

前四道题目的解法基本一样,最后一道题目稍复杂~

我们先来看最基础的这道题目121. 买卖股票的最佳时机

在这里插入图片描述
121题中限制了只能进行一次股票交易(买入并卖出),下面以本题讲解动态规划的状态设计和转移方程。

n = l e n ( p r i c e s ) n=len(prices) n=len(prices) 表示天数。

二、动规思路

1.状态设计

状态值就是收益值,关键是设计 “什么情况下” 的收益值。我们假设:

d p [ i ] [ 0 ] dp[i][0] dp[i][0] 表示第 i i i天结束时,手中不持有股票的最大收益;
d p [ i ] [ 1 ] dp[i][1] dp[i][1] 表示第 i i i天结束时,手中持有股票的最大收益;
(为了与数组的下标保持一致, i i i从0开始)

这样我们的输出应该是 d p [ n − 1 ] [ 0 ] dp[n-1][0] dp[n1][0],因为最大收益一定是在最后一天手上不持有股票的情况下获得的。 这句话对系列中所有题目均适用!

2.状态初始化

在第0天时,若不持有股票,那么收益是0;若持有股票,收益是 − p r i c e s [ 0 ] -prices[0] prices[0],因为我们花钱买入了股票。

d p [ 0 ] [ 0 ] = 0 , d p [ 0 ] [ 1 ] = − p r i c e s [ 0 ] dp[0][0]=0,dp[0][1]=-prices[0] dp[0][0]=0dp[0][1]=prices[0]

3.状态转移方程

(1)对于 d p [ i ] [ 0 ] dp[i][0] dp[i][0],有两种情况可以导致在第 i i i天结束时,手中不持有股票:

  • 在第 i − 1 i-1 i1天结束时,手中就不持有股票,在第 i i i天也没有买入股票,此时收益是 d p [ i − 1 ] [ 0 ] dp[i-1][0] dp[i1][0]
  • 在第 i − 1 i-1 i1天结束时,手中持有股票,但是在第 i i i天时卖出了股票,此时收益是 d p [ i − 1 ] [ 1 ] + p r i c e s [ i ] ) dp[i-1][1]+prices[i]) dp[i1][1]+prices[i])

综合两种情况,我们求最大收益,因此状态转移为:

d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] + p r i c e s [ i ] ) dp[i][0]=max(dp[i-1][0], dp[i-1][1]+prices[i]) dp[i][0]=max(dp[i1][0],dp[i1][1]+prices[i])

(2)对于 d p [ i ] [ 1 ] dp[i][1] dp[i][1],同样有两种情况可以导致在第 i i i天结束时,手中持有股票:

  • 在第 i − 1 i-1 i1天结束时,手中就持有股票,在第 i i i天也没有卖出股票,收益即为 d p [ i − 1 ] [ 1 ] dp[i-1][1] dp[i1][1]
  • 在第 i − 1 i-1 i1天结束时,手中不持有股票,但是在第 i i i天时买入了股票。但是此时需要注意,我们只能进行一次股票交易,若在第 i i i天买入股票,那么之前的收益都不作数,要从当前开始重新计算收益。因此收益是 − p r i c e s [ i ] -prices[i] prices[i]

综合两种情况,我们求最大收益,因此状态转移为:

d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] , − p r i c e s [ i ] ) dp[i][1]=max(dp[i-1][1], -prices[i]) dp[i][1]=max(dp[i1][1],prices[i])

4.完整代码(python)

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices: return 0  # 注意prices为空的情况
        n = len(prices)
        dp = [[0,-prices[0]]] + [[0,0] for _ in range(n)]
        for i in range(1, n):
            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.不限制交易次数

题目描述

对应Leetcode 122题:
在这里插入图片描述

思路解析

我们不改变状态设计方式,只需要看状态转移方程如何变化:

经过简单分析,可以发现状态初始化是不变的~本题和121题的区别在于,不限制交易次数,这对 d p [ i ] [ 1 ] dp[i][1] dp[i][1]的状态转移会有影响。
在限制一次交易次数时,我们需要对 d p [ i ] [ 1 ] dp[i][1] dp[i][1]的状态转移进行限制,买入股票时收益必须重新计算;那在不限制次数时,买入股票时的收益就可以累加到前面的收益上。

所以我们只需要改变 d p [ i ] [ 1 ] dp[i][1] dp[i][1]的状态转移方程即可:

d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] , d p [ i − 1 ] [ 0 ] − p r i c e s [ i ] ) dp[i][1]=max(dp[i-1][1], dp[i-1][0]-prices[i]) dp[i][1]=max(dp[i1][1],dp[i1][0]prices[i])

d p [ i ] [ 0 ] dp[i][0] dp[i][0]的状态转移不变,仍是 d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] + p r i c e s [ i ] ) dp[i][0]=max(dp[i-1][0], dp[i-1][1]+prices[i]) dp[i][0]=max(dp[i1][0],dp[i1][1]+prices[i])

完整代码(python)

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n = len(prices)
        dp = [[0,-prices[0]]] + [[0,0] for _ in range(n)]
        for i in range(1, n):
            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]

2.含冰冻期的交易

题目描述

对应Leetcode 309题:
在这里插入图片描述

思路解析

读题发现,它没有限制交易次数,但加入了冰冻期的限制,因此我们在122题的基础上,思考状态转移方程应如何变化。

冰冻期对于买入有限制,要求在卖出后的一天无法买入股票;对于股票卖出无限制。因此状态转移时考虑改变 d p [ i ] [ 1 ] dp[i][1] dp[i][1]的状态转移方程,因为在 d p [ i ] [ 1 ] dp[i][1] dp[i][1]状态转移时出现了买入股票的情况:

对于 d p [ i ] [ 1 ] dp[i][1] dp[i][1],有两种情况可以导致在第 i i i天结束时,手中持有股票:

  • 在第 i − 1 i-1 i1天结束时,手中就持有股票,在第 i i i天也没有卖出股票,收益即为 d p [ i − 1 ] [ 1 ] dp[i-1][1] dp[i1][1]
  • 在第 i − 2 i-2 i2天结束时,手中不持有股票,但是在第 i i i天时买入了股票。为什么是从第 i − 2 i-2 i2天开始,不持有股票呢?此时需要注意冰冻期的存在,若在第 i i i天可以买入股票,说明第 i − 2 i-2 i2天就卖出了股票,在第 i − 2 i-2 i2天结束时手中不持有股票。因此最大收益是 d p [ i − 2 ] [ 0 ] − p r i c e s [ i ] dp[i-2][0]-prices[i] dp[i2][0]prices[i]

综合两种情况,我们求最大收益,因此状态转移为:

d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] , d p [ i − 2 ] [ 0 ] − p r i c e s [ i ] ) dp[i][1]=max(dp[i-1][1], dp[i-2][0]-prices[i]) dp[i][1]=max(dp[i1][1],dp[i2][0]prices[i])

d p [ i ] [ 0 ] dp[i][0] dp[i][0]的状态转移不变。

完整代码(python)

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

        return dp[n-1][0]

3.含手续费的交易

题目描述

对应Leetcode 714题:

在这里插入图片描述

思路解析

这道题不限制交易次数,但加入了交易的手续费,我们仍在122题的基础上思考。

由于一次交易,包括买卖两个行为,只需要支付一次手续费,那么我们假设在卖出股票时支付,因此需要修改 d p [ i ] [ 0 ] dp[i][0] dp[i][0]的状态转移方程: (因为 d p [ i ] [ 0 ] dp[i][0] dp[i][0]的状态转移过程中包括了卖股票的行为)

对于 d p [ i ] [ 0 ] dp[i][0] dp[i][0],有两种情况可以导致在第 i i i天结束时,手中不持有股票:

  • 在第 i − 1 i-1 i1天结束时,手中就不持有股票,在第 i i i天也没有买入股票,此时收益是 d p [ i − 1 ] [ 0 ] dp[i-1][0] dp[i1][0]
  • 在第 i − 1 i-1 i1天结束时,手中持有股票,但是在第 i i i天时卖出了股票,我们获得了卖股票的钱,同时要扣除手续费,此时收益是 d p [ i − 1 ] [ 1 ] + p r i c e s [ i ] − f e e ) dp[i-1][1]+prices[i]-fee) dp[i1][1]+prices[i]fee)

综合两种情况,我们求最大收益,因此状态转移为:

d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] + p r i c e s [ i ] − f e e ) dp[i][0]=max(dp[i-1][0], dp[i-1][1]+prices[i]-fee) dp[i][0]=max(dp[i1][0],dp[i1][1]+prices[i]fee)

d p [ i ] [ 1 ] dp[i][1] dp[i][1]的状态转移不变, d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] , d p [ i − 1 ] [ 0 ] − p r i c e s [ i ] ) dp[i][1]=max(dp[i-1][1], dp[i-1][0]-prices[i]) dp[i][1]=max(dp[i1][1],dp[i1][0]prices[i])

完整代码(python)

class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        n = len(prices)
        dp = [[0,-prices[0]]] + [[0, 0] for _ in range(n-1)]
        for i in range(1, n):
            dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i]-fee)
            dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i])
        return dp[n-1][0]

4.限制交易不超过两次

题目描述

对应Leetcode 123题:
在这里插入图片描述
题目限制交易不超过两次,可以不进行交易,或者交易1次、2次。

思路解析

1、状态设计
在这样的限制条件下,原来的状态设计不能满足需求了,我们需要扩充一维来表示当前已交易的次数。因此状态更改为:

d p [ i ] [ 0 ] [ 0 ] dp[i][0][0] dp[i][0][0] :在第 i i i天结束时,交易了0次且手上不持有股票情况下,最大的收益;

d p [ i ] [ 0 ] [ 1 ] dp[i][0][1] dp[i][0][1] :在第 i i i天结束时,交易了1次且手上不持有股票情况下,最大的收益;

d p [ i ] [ 0 ] [ 2 ] dp[i][0][2] dp[i][0][2] :在第 i i i天结束时,交易了2次且手上不持有股票情况下,最大的收益;

d p [ i ] [ 1 ] [ 0 ] dp[i][1][0] dp[i][1][0] :在第 i i i天结束时,交易了0次且手上持有股票情况下,最大的收益;

d p [ i ] [ 1 ] [ 1 ] dp[i][1][1] dp[i][1][1] :在第 i i i天结束时,交易了1次且手上持有股票情况下,最大的收益;

d p [ i ] [ 1 ] [ 2 ] dp[i][1][2] dp[i][1][2] :在第 i i i天结束时,交易了2次且手上持有股票情况下,最大的收益;

最终的最大收益可能是在交易了0、1、2次后,最后一天手上不持股的收益,我们取其最大值,即:

m a x ( d p [ n − 1 ] [ 0 ] [ 0 ] , d p [ n − 1 ] [ 0 ] [ 1 ] , d p [ n − 1 ] [ 0 ] [ 2 ] ) max(dp[n-1][0][0], dp[n-1][0][1], dp[n-1][0][2]) max(dp[n1][0][0],dp[n1][0][1],dp[n1][0][2])

我们需要考虑以上六种状态的转移方程。

2、状态转移

其中, d p [ i ] [ 0 ] [ 0 ] dp[i][0][0] dp[i][0][0] d p [ i ] [ 1 ] [ 2 ] dp[i][1][2] dp[i][1][2] 在任何 i i i取值下,值都为0。 因为交易0次且手上不持股时,必然没有收益;而交易了2次但还持股是不满足要求的,我们不能进行第三次交易,因此这种情况的收益直接定为0,不在考虑范围内。这时,还剩下四种状态需要写出转移方程。

根据前面四道题目的分析,我们可以按照相同的思路写出转移方程,我就不推导了╰( ̄ω ̄o)

对于每种状态,都分两种情况讨论(是不是当天买入/卖出的),并在二者中取最大值

d p [ i ] [ 0 ] [ 1 ] = m a x ( d p [ i − 1 ] [ 0 ] [ 1 ] , d p [ i − 1 ] [ 1 ] [ 0 ] + p r i c e s [ i ] ) dp[i][0][1] = max(dp[i-1][0][1],dp[i-1][1][0]+prices[i]) dp[i][0][1]=max(dp[i1][0][1],dp[i1][1][0]+prices[i])

d p [ i ] [ 0 ] [ 2 ] = m a x ( d p [ i − 1 ] [ 0 ] [ 2 ] , d p [ i − 1 ] [ 1 ] [ 1 ] + p r i c e s [ i ] ) dp[i][0][2] = max(dp[i-1][0][2],dp[i-1][1][1]+prices[i]) dp[i][0][2]=max(dp[i1][0][2],dp[i1][1][1]+prices[i])

d p [ i ] [ 1 ] [ 0 ] = m a x ( d p [ i − 1 ] [ 1 ] [ 0 ] , d p [ i − 1 ] [ 0 ] [ 0 ] − p r i c e s [ i ] ) dp[i][1][0] = max(dp[i-1][1][0],dp[i-1][0][0]-prices[i]) dp[i][1][0]=max(dp[i1][1][0],dp[i1][0][0]prices[i])

d p [ i ] [ 1 ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] [ 1 ] , d p [ i − 1 ] [ 0 ] [ 1 ] − p r i c e s [ i ] ) dp[i][1][1] = max(dp[i-1][1][1],dp[i-1][0][1]-prices[i]) dp[i][1][1]=max(dp[i1][1][1],dp[i1][0][1]prices[i])

3、状态初始化
不论已经存在几次交易,在第0天结束时手上不持股的收益均为0,手上持股的收益都是 − p r i c e s [ 0 ] -prices[0] prices[0],即
d p [ 0 ] [ 0 ] [ 0 ] = d p [ 0 ] [ 0 ] [ 1 ] = d p [ 0 ] [ 0 ] [ 2 ] = 0 dp[0][0][0]=dp[0][0][1]=dp[0][0][2]=0 dp[0][0][0]=dp[0][0][1]=dp[0][0][2]=0

d p [ 0 ] [ 1 ] [ 0 ] = d p [ 0 ] [ 1 ] [ 1 ] = d p [ 0 ] [ 1 ] [ 2 ] = − p r i c e s [ 0 ] dp[0][1][0]=dp[0][1][1]=dp[0][1][2]=-prices[0] dp[0][1][0]=dp[0][1][1]=dp[0][1][2]=prices[0]

完整代码(python)

这里要初始化三维列表,一定要仔细。

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n = len(prices)
        dp = [[[0 for _ in range(3)],[-prices[0] for _ in range(3)]]] + [[[0 for _ in range(3)],[0 for _ in range(3)]] for _ in range(n)]

        for i in range(1, n):
            dp[i][0][0] = 0     # 或者写成 dp[i-1][0][0]
            dp[i][0][1] = max(dp[i-1][0][1],dp[i-1][1][0]+prices[i])
            dp[i][0][2] = max(dp[i-1][0][2],dp[i-1][1][1]+prices[i])
            dp[i][1][0] = max(dp[i-1][1][0],dp[i-1][0][0]-prices[i])
            dp[i][1][1] = max(dp[i-1][1][1],dp[i-1][0][1]-prices[i])
            dp[i][1][2] = 0
        
        return max(dp[n-1][0][0], dp[n-1][0][1], dp[n-1][0][2])

这里要新加入一题,是今天看到的,Leetcode188 买卖股票的最佳时机 IV,题目如下:

在这里插入图片描述
它和上一道题很相近,只是将最多完成两笔交易改成了最多完成 k k k笔交易。按照我们上一题的思路,我们只需将 d p dp dp的维数变为 n × ( k + 1 ) × 2 n×(k+1)×2 n×(k+1)×2。从上一题的状态更新中我们可以看出,不论交易次数的值是多少,状态更新方程的形式都是一样的。因此,我们可以在更新状态时,同时循环天数与交易次数。

还要注意 d p dp dp的初始化。我在写代码时,初始化了 d p [ : ] [ 0 ] [ 0 ] , d p [ : ] [ 0 ] [ 1 ] , d p [ 0 ] [ : ] [ 1 ] dp[:][0][0], dp[:][0][1], dp[0][:][1] dp[:][0][0],dp[:][0][1],dp[0][:][1],下面附上完整代码:

class Solution:
    def maxProfit(self, k: int, prices: List[int]) -> int:
        if not prices: return 0
        n = len(prices)
        dp = [[[0, 0] for _ in range(k+1)] for _ in range(n)]
        cur_max = float('-inf')
        for j in range(k+1):
            dp[0][j][1] = -prices[0]
        for i in range(n):
            dp[i][0][1] = max(cur_max, -prices[i])
            cur_max = dp[i][0][1]

        for i in range(1, n):
            for j in range(1, k+1):
                dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j-1][1]+prices[i])
                dp[i][j][1] = max(dp[i-1][j][1], dp[i-1][j][0]-prices[i])

        res = dp[-1][0][0]

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


总结

这些动规方法都是可以进一步对空间进行优化的,大家可以自行尝试。除了动规外,还有时间、空间复杂度更小的算法,不过就不具有统一性了,可以在leetcode题解中去学习~

内容有点多,可能会笔误,大家发现哪里有写错的地方记得告诉我哦(ง •_•)ง

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值