买卖股票的最jia时机
(标题本来是 “最佳”,违反了标题规定哈哈哈,被当成标题党了。)
给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0
。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
思路:
股票买卖问题的一大类非常经典的题目。所有的股票买卖问题都可以用一个模板去解决。我们需要一个三维的动态规划数组来实现这个模板(此模板参考《labuladong的算法小抄》)。
模板(理解并记忆!):
dp[i][k][0]=max(dp[i-1][k][0],dp[i-1][k][1]+prices[i])
dp[i][k][1]=max(dp[i-1][k][1],dp[i-1][k-1][0]-prices[i])
1.首先清楚dp三维数组的定义:
其中第一位表示第几天,第二位表示交易次数,第三位表示是否持有股票。
dp[i][k][j]表示第i天,总交易次数为k时,且持有情况为j时的最大利润。
比如:
dp[i][k][0]表示第i天,总交易次数为k,未持有股票(0)时的利润
dp[i-1][k][1]表示第i天,总交易次数k,持有股票(1)时的利润
2.其次清楚状态转移:
对于第一句:dp[i][k][0]=max(dp[i-1][k][0],dp[i-1][k][1]+prices[i])
意思是如果我们第i天没有持有股票(0),则要么我们前一天(i-1)也不持有,要么就在i-1天时持有但是卖出了。前者对应着dp[i-1][k][0],后者对应dp[i-1][k][1]+prices[i](卖出,当天股票价格多少就加多少钱)。其中哪个利润较大我们取哪个,用max。
对于第二句:dp[i][k][1]=max(dp[i-1][k][1],dp[i-1][k-1][0]-prices[i])
意思是如果我们第i天持有了股票(1),则要么我们前一天(i-1)也持有着保持到选择,要么就是在i-1天时没有持有但买入了。前者对应dp[i-1][k][1],后者对应dp[i-1][k-1][0]-prices[i](买入,当天股票价格多少钱,就减多少钱)。其中哪个利润较大我们取哪个,用max。
上面还需要注意的是我们k的变化,k代表着交易次数。在第一句中,我们有卖出的操作,但k其实并没有变动;在第二句中,买入股票时我们将交易次数k-1了。因为买入和卖出,才只凑成一套交易,因此只能选择在其中一个地方减少我们的交易次数。习惯上来看,就什么时候买了新股票,我们什么时候把交易次数减1即可。
清楚了以上的动态规划数组的定义和转移方式,基本上任何股票买卖的问题都可以通过此方式解决了。而对于一些简单的股票买卖问题变式,我们其实也并不需要这么多参数。
比如在这一题当中,因为我们交易次数只有一次,所以实际上模板就等同于:
dp[i][1][0]=max(dp[i-1][1][0],dp[i-1][1][1]+prices[i])
dp[i][1][1]=max(dp[i-1][1][1],dp[i-1][0][0]-prices[i])
因为总交易次数k为0时,利润必然必然为0,所以可得:
dp[i][1][0]=max(dp[i-1][1][0],dp[i-1][1][1]+prices[i])
dp[i][1][1]=max(dp[i-1][1][1], -prices[i])
可以发现第二维全都是1,并无状态转移意义,因此第2维可以直接删除。因此,状态转移方程变最终为:
dp[i][0]=max(dp[i-1][0],dp[i-1][1]+prices[i])
dp[i][1]=max(dp[i-1][1], -prices[i])
最后,我们还要注意这个二维数组(删除了1维,不再是三维了)的初始化以及base case的定义。我们初始化所有值为空,特别声明dp[0][1]为负无穷,因为第0天显然不可能持有。
需要注意,我们这里第1天就是dp[1],并不是从dp[0],这样方便理解。另外第一天的股票价格因为我们就是用的prices数组,得通过prices[0]来得到,因此在下面代码中状态转移方程prices的下标会有个-1,和模板中prices[i]表示第i天的价格稍有不同,这里知道就好。
代码:
class Solution(object):
def maxProfit(self, prices):
# dp[i][k][0]=max(dp[i-1][k][0],dp[i-1][k][1]+prices[i])#老模板,放在最上面参考
# dp[i][k][1]=max(dp[i-1][k][1],dp[i-1][k-1][0]-prices[i])
lenth = len(prices)#获取一共有几天(有几个价格就有几天)
dp=[[0]*2 for _ in range(lenth+1)]#初始化二维数组,长为lenth+1,宽为2
dp[0][1]=-float('inf')#定义base case 第0天却持有的情况不可能,初始化为负无穷
for i in range(1,lenth+1):#从第一天开始向上生成
dp[i][0]=max(dp[i-1][0],dp[i-1][1]+prices[i-1])#使用我们简化的模板
dp[i][1]=max(dp[i-1][1],-prices[i-1])
return dp[lenth][0]#返回的就是最后一天,且未持有时的利润
#毕竟最后肯定要卖出嘛,所以肯定是未持有
小结:
我建议做股票买卖问题的时候,无论如何都先把我们最万能的终极公式写出来:
dp[i][k][0]=max(dp[i-1][k][0],dp[i-1][k][1]+prices[i])
dp[i][k][1]=max(dp[i-1][k][1],dp[i-1][k-1][0]-prices[i])
然后我们根据题意去看看是否需要简化我们的状态转移方程,比如这一题我们知道交易次数只有1次,所以我们就把第二个参数去掉。
最后补两个特别注意点:
一、股票买卖问题base case的定义也十分重要。根据我们的定义方式,我们知道1、第0天不可能持有,后面遇到其他题型时,我们还知道2、交易次数为0时不可能持有。对于不可能的情况我们定义成负无穷-float(‘inf’),这样此类情况在与其他合法情况做max时就不可能被选出了。这一题只用到了第一条,第二条以后其他变式会用到。
二、还要特别注意:对于最终结果的选取,我们是取dp[lenth][k][0],而不是dp[lenth][0][0]。因为第二维交易次数k并不是“当前剩余次数”,而是总次数。所以实际上在这一题,如果我们不删掉第二维,则结果取得应该是dp[lenth][1][0] (总交易次数为1)。这一要点也在以后其他变式题中会用到。
所有的股票问题统一做一遍,就会发现其实都差不多。这一题我使用的方式是二维动态规划这样比较便于理解的方式,为了更小的空间复杂,我们可以压缩至一维——但那是在理解二维的基础上去修改代码。假如一开始就要去看最简单的写法,则几乎不可能理解其真正的意义,而且那种写法也不好记忆,只能是看别人写出来炫技罢了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了