股票问题是动态规划里面非常非常经典的问题了,本文列举了8道经典题目,都给出了详细的分析。可以发现这些题目的思路都是一致的,只是细节不同而已!赶快刷起来~
\quad
PS. 本文是参考代码随想录做的一些笔记,完整版本请戳链接,非常好的教程!
给定一个数组 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]
给你一个整数数组 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
给定一个数组,它的第 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]
也有两个操作:
dp[i][2] = dp[i - 1][1] + prices[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]
,所以这些都要初始化!
dp[0][0] = 0
;dp[0][1] = -prices[0]
;dp[0][2] = 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]
注意:上面的四个递推公式其实有规律在其中的,所以上面的代码这么简洁!发现了这个规律,那么下一题就简单了~
给定一个整数数组 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]
给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
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]
,买入股票所剩现金为负数。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:])
给定一个整数数组 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]
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
持续更新…