文章目录
前言
股票买卖问题是一类常见问题,这个系列包括五道题目(仅限于我做过的(+_+)),都是给定某几天的股票价格,要求求出通过交易能获得的最大收益。
每个题都有不同的解决方案,但是同样的动态规划思路可以直接解决这个系列。这篇博文就是介绍这个简单却通吃的动规思路哒~
注:本文参考leetcode上各种题解,大家感兴趣可以去看看,这些题目各自特点不同,因此每道题目都有自己独特的解法,这里不做讨论。本文只是介绍最简单通用的动规方法,且不包括动规的化简(●’◡’●)
一、题目描述
股票系列包括:
121. 买卖股票的最佳时机 简单
122. 买卖股票的最佳时机 II 简单
309. 最佳买卖股票时机含冷冻期 中等
714. 买卖股票的最佳时机含手续费 中等
前四道题目的解法基本一样,最后一道题目稍复杂~
我们先来看最基础的这道题目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[n−1][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]=0,dp[0][1]=−prices[0]
3.状态转移方程
(1)对于 d p [ i ] [ 0 ] dp[i][0] dp[i][0],有两种情况可以导致在第 i i i天结束时,手中不持有股票:
- 在第 i − 1 i-1 i−1天结束时,手中就不持有股票,在第 i i i天也没有买入股票,此时收益是 d p [ i − 1 ] [ 0 ] dp[i-1][0] dp[i−1][0];
- 在第 i − 1 i-1 i−1天结束时,手中持有股票,但是在第 i i i天时卖出了股票,此时收益是 d p [ i − 1 ] [ 1 ] + p r i c e s [ i ] ) dp[i-1][1]+prices[i]) dp[i−1][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[i−1][0],dp[i−1][1]+prices[i])
(2)对于 d p [ i ] [ 1 ] dp[i][1] dp[i][1],同样有两种情况可以导致在第 i i i天结束时,手中持有股票:
- 在第 i − 1 i-1 i−1天结束时,手中就持有股票,在第 i i i天也没有卖出股票,收益即为 d p [ i − 1 ] [ 1 ] dp[i-1][1] dp[i−1][1];
- 在第 i − 1 i-1 i−1天结束时,手中不持有股票,但是在第 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[i−1][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[i−1][1],dp[i−1][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[i−1][0],dp[i−1][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 i−1天结束时,手中就持有股票,在第 i i i天也没有卖出股票,收益即为 d p [ i − 1 ] [ 1 ] dp[i-1][1] dp[i−1][1];
- 在第 i − 2 i-2 i−2天结束时,手中不持有股票,但是在第 i i i天时买入了股票。为什么是从第 i − 2 i-2 i−2天开始,不持有股票呢?此时需要注意冰冻期的存在,若在第 i i i天可以买入股票,说明第 i − 2 i-2 i−2天就卖出了股票,在第 i − 2 i-2 i−2天结束时手中不持有股票。因此最大收益是 d p [ i − 2 ] [ 0 ] − p r i c e s [ i ] dp[i-2][0]-prices[i] dp[i−2][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[i−1][1],dp[i−2][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 i−1天结束时,手中就不持有股票,在第 i i i天也没有买入股票,此时收益是 d p [ i − 1 ] [ 0 ] dp[i-1][0] dp[i−1][0];
- 在第 i − 1 i-1 i−1天结束时,手中持有股票,但是在第 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[i−1][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[i−1][0],dp[i−1][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[i−1][1],dp[i−1][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[n−1][0][0],dp[n−1][0][1],dp[n−1][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[i−1][0][1],dp[i−1][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[i−1][0][2],dp[i−1][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[i−1][1][0],dp[i−1][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[i−1][1][1],dp[i−1][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题解中去学习~
内容有点多,可能会笔误,大家发现哪里有写错的地方记得告诉我哦(ง •_•)ง