概述
熟悉这类题目的同学都知道,事实上这是一类动态规划(dp)问题,在小专题中都暂且只针对这一类问题进行解决了,更全面的动态规划文章以后有机会再努力吧!
简单介绍一句动态规划。动态规划实际是一种分治思想,与分治算法不同的是,分治算法是把原问题分解为若干子问题,自顶向下求解各子问题,合并子问题的解,从而得到原问题的解。动态规划也是把原问题分解为若干子问题,然后自底向上,先求解最小的子问题,把结果存储在表格中,再求解大的子问题时,直接从表格中查询小的子问题的解,避免重复计算,从而提高算法效率。
对于本类问题而言,我们采用有限状态自动机的思想来解决,实际就是找到所有状态,并根据状态转移图来列状态转移方程,实际还是dp的思想,下面我们以第一道题为例来介绍。
例题
我们以121.买卖股票的最佳时机为例来进行讲解。题目如下:
题目
给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0
。
示例 :
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
分析
首先我们归纳一下题意:每天股票价格在数组prices
中,每天可以选择买入、卖出或是什么都不做。只有没有股票时才能买入,只有有股票时才能卖出,而且只能买入(卖出)一次,求最大利润。
下面我们需要明确几件事:
- 整个股票买卖过程可以进行哪些操作(选择)?每一天都可以买入、卖出、休息(既不买入也不卖出),只不过买入和卖出是有要求的,必须先买入才能卖出。
- 既然采用自动机来解决,那么过程中有那些状态呢?因为可以进行以上三种操作,因此每一天有两种状态:持有股票和不持有股票。今天持有股票是因为之前或今天买入了股票且没有卖出;今天不持有股票是因为之前或今天一直没买股票或者已经卖出了。
- 最大利润在哪里产生?一定是在最后一天的不持有股票状态。(显然把股票卖掉一定比手里拿着股票获利更多)
我们根据是否持有股票这两个状态画出如下图所示的状态转移图。图中状态0
表示当前不持有股票,状态1
表示当前持有股票。

对上图的转移过程的解释如下:当我们不持有股票时,可以通过买入buy
变为持有状态;也可以休息rest
仍然为不持有状态。当我们持有股票时,可以通过卖出sale
变为不持有状态;也可以休息rest
仍然为持有状态。
有了状态转移图,我们应该考虑一种数据结构来记录每个时刻(每天)的状态(股票的持有状态)。因此使用一个二维数组来记录,记为dp数组。举例:dp[i][0]
表示第i
天时不持有股票时的最大利润,dp[i][1]
表示第i
天时持有股票的最大利润。然后写出状态转移方程,也就是上述两个式子是怎么由其他状态转换来的,怎么求出来的。
以dp[i][0]
为例,什么情况下第i
天才能不持有股票呢?无非就是两种情况:
- 第
i-1
天也是不持有股票,然后第i
天也没有买入,此时利润仍然等于第i-1
天的最大利润; - 第
i-1
天持有了股票,但第i
天进行卖出了,此时利润等于第i-1
天持有股票的利润加上卖出股票的获利。(因为股票只能买卖一次,因此第i-1
天持有股票的利润就是之前买股票的花费)
因此dp[i][0]
应该取以上两种情况的最大值,即dp[i][0]=max(dp[i-1][0],dp[i-1][0]+prices[i])
。
同理,以下两种情况下第i
天会持有股票:
- 第
i-1
天就已经持有股票,然后第i
天没有卖出,此时利润仍然等于第i-1
天的最大利润; - 第
i-1
天不持有股票,但第i
天买入了股票,此时利润等于买入股票的价格的相反数。(因为股票只能买卖一次,因此第i
天买入股票的利润就是当天买入股票的花费)
因此,dp[i][1]=max(dp[i-1][1],-prices[i])
。
这样我们就已经写出状态转移方程了,下面要思考初始的状态(初态,显然方程中i
不能为0
,因此我们把i=0
时的情况分析出来,剩下的就从i=1
开始直接递推了)。
dp[0][0]=0
:第1
天不持有股票,因此利润是0
;dp[0][1]=prices[0]
:第1
天持有股票,即第一天买了股票,利润是第一天股票价格的相反数。
参考代码
class Solution {
public int maxProfit(int[] prices) {
int [][]dp=new int[prices.length][2];
dp[0][0]=0;// 第1天不持有股票,利润为0
dp[0][1]=-prices[0];// 第1天持有股票,利润为第一天股票售价的相反数
for(int i=1;i<prices.length;i++){
// 第i天如果没持有股票,对应两种情况:1.前一天就是没有持有股票的状态2.今天把前一天持有的股票给卖了,额外获利今天的股票价格。
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
// 第i天如果持有股票,对应两种情况:1.前一天就是持有股票的状态2.今天刚刚买入股票,额外加上今天股票价格的支出。
dp[i][1]=Math.max(dp[i-1][1],-prices[i]);
}
return dp[prices.length-1][0];// 最大利润产生在【最后一天的不持有股票状态】
}
}
代码优化
观察上面的代码我们发现,事实上每一天的利润只与上一天的利润有关,最后需要的答案也只是最后一天的不持有股票状态,因此完全可以不用dp
数组记录每天的每个状态,只需要用变量来记录上一天的每个状态就可以了,这样能使空间复杂度降为O(1)
。核心思想是不变的,只是本题并不需要记录所有状态,只需要保证有足够的数据(前一天的状态数据)来递推出最后一天的情况即可。
即:使用变量dp0
来代替dp
数组中每天不持有股票的情况(dp[i][0]
),变量dp1
代替dp
数组中每天持有股票的情况(dp[i][1]
)。
优化后的代码如下:
class Solution {
public int maxProfit(int[] prices) {
//int [][]dp=new int[prices.length][2];
int dp0=0;
int dp1=-prices[0];
for(int i=1;i<prices.length;i++){
dp0=Math.max(dp0,dp1+prices[i]);
dp1=Math.max(dp1,-prices[i]);
}
return dp0;
}
}
总结
使用自动机的思想,找到股票买卖问题中的三种操作:买入、卖出、休息,两个状态:持有股票、不持有股票,然后画出状态转换图,根据转换图去写出状态转移方程。使用合适的数据结构去记录状态,本题中只有两个变化的状态,天数和持有情况,因此使用二维数组dp
记录每天的持有情况就可以了。要额外考虑一下初态的情况,剩下的就交给循环,递推求解即可。明确最后的结果产生在最后一天的不持有股票状态。
后面还会有增加一个状态的变式:即不再限制只能买卖一次,可以买多次、任意次等,我们增加一个状态即可;还会限制买卖间隔时间,我们修改状态转换图即可。以下列出的题目我都会在本专题第二篇—刷题部分去讲解,到时更新了我会把链接放在这里。
Letcode中股票买卖问题(点击链接跳转去看题):
121.买卖股票的最佳时机(简单):即本文例题,最多只买卖一次。
122.买卖股票的最佳时机II(简单):可以多次买卖,不做任何限定。
123.买卖股票的最佳时机III(困难):最多只能买卖两次。
188.买卖股票的最佳时机IV(困难):最多可以买卖k
次。k
是变化的。
309.最佳买卖股票时机含冷冻期(中等):卖出和买入之间必须至少间隔一天。
714.买卖股票的最佳实际含手续费(中等):可以多次买卖,但每次买入股票都要额外支付手续费。
参考
- labuladong的算法小抄。链接
更多文章
更多文章仍在更新,欢迎收藏本专栏。感谢您的每一个喜欢~