题目来源
Leecode.题库
122.买卖股票的最佳时机II
题目描述
买卖股票的最佳时机 II 给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。输入: prices = [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格= 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。输入: prices = [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格= 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。 示例 3:输入: prices = [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。提示:
1 <= prices.length <= 3 * 1 0 4 10^{4} 104
0 <= prices[i] <= 1 0 4 10^{4} 104
题目分析
题目给了一串数组作为股票在一段时间内的价格,看似教我们赚钱,实际上同时只能买一支股票,那么无非就是低买高卖。买卖次数不限,让我们找出这段时间能赚的最多的买卖方法。这其中需要考虑的问题就是时机。
解决办法
动态规划
动态规划就是先假设只有两三个数字(最简单的情况),先算出不同的状态的最大值,然后一个一个向模型中加入数字,利用之前的计算结果慢慢地推出最后的结果。
通常我们使用一个很大的数组,然后按照遍历一步步算出dp[i]的值,前面的结果往往会对后面产生指导作用。
我们知道,在这个游戏中的小人有两种状态,
- 没买股票
- 买了股票
如果我们把0和1当成一次操作,可以发现,对于任意的数列,我们有:
- 当0→1时,买入了股票,没有收入。
- 当0→0时,无事发生,没有收入。
- 当1→0时,卖出了股票,有收入。
- 当1→1时,不可能同时买入两支股票。
这个时候我们可以建立二维数组 d p [ p r i c e S i z e ] [ 2 ] dp[priceSize][2] dp[priceSize][2]模拟每种情况下的最大收入,其中第一维度指代模型中最后一个数据的下标,第二维表示两种状态。
int dp[pricesSize][2];
dp[0][0]=0;dp[0][1]=0;
我们首先把第一个数据
p
r
i
c
e
s
[
0
]
prices[0]
prices[0]放入动态模型,当我们没有买它,我们得到0元;买了,同样得到0元(因为我们只有一个数据,还没有卖出)。
从第二个数据开始,我们可以得到:
d
p
[
i
]
[
1
]
=
d
p
[
i
−
1
]
[
0
]
;
dp[i][1]=dp[i-1][0];
dp[i][1]=dp[i−1][0];
如果我们准备在这次买下股票,那么我们最大的收入就是上一次没有买股票时收入最大值
d
p
[
i
−
1
]
[
0
]
dp[i-1][0]
dp[i−1][0](因为上一次买了这一次就不能买)。
而如果我们这次没有买股票,我们可以选择:
- 上次没买的最大收入 d p [ i − 1 ] [ 0 ] dp[i-1][0] dp[i−1][0](两次都没买);
- 从之前买进的某一次股票开始卖出 p r i c e s [ i ] − p r i c e s [ j ] + d p [ j ] [ 1 ] prices[i]-prices[j]+dp[j][1] prices[i]−prices[j]+dp[j][1]
第二个选择可能有点抽象,我们知道如果一直持有股票没有卖的话,这期间是没有收入的,所以直接记上次买了之后的最大收入,加上这次交易的收入,因为我们有很多的选择(买之前的任何股票再卖),这里需要一个遍历。
for(i=1;i<pricesSize;i++)
{
dp[i][1]=dp[i-1][0];
int max=dp[i-1][0];
for(j=0;j<i;j++) if(max<prices[i]-prices[j]+dp[j][1])
max=prices[i]-prices[j]+dp[j][1];
dp[i][0]=max;
}
这就是状态转移方程,我们不考虑之前到底是怎样买卖,只留下了最大利润的数字,有了动态规划的数组之后,我们只需要考虑最后一次交易,这就是一个纯数据的比较。
总的函数代码如下
int maxProfit(int* prices, int pricesSize){
if(pricesSize==1) return 0;
//状态转移方程的动态规划
int dp[pricesSize][2];
int i,j;
//0没买1买了
dp[0][0]=0;dp[0][1]=0;
for(i=1;i<pricesSize;i++)
{
dp[i][1]=dp[i-1][0];
int max=dp[i-1][0];
for(j=0;j<i;j++) if(max<prices[i]-prices[j]+dp[j][1])
max=prices[i]-prices[j]+dp[j][1];
dp[i][0]=max;
}
return dp[pricesSize-1][0];
}
结果判定:
设置哨兵
我发现了滑点(不是) 想了想,在股票世界里就是起起伏伏,既然我们可以不断买卖,而且还预知未来,为什么不把每一次“起”都收入囊中呢,只需要避开所有的“跌”就不会亏呀。
如上图所示,只要在所有的上行线上都保持“持有”的状态,就可以稳定增值,即:最低点买入,最高点卖出。
代码实现的时候有几个注意点,如何判断是最低/最高点?
这个时候就一直用选择判断筛选。我们可以一直保持一个较低的点,作为
p
h
e
a
d
phead
phead,一直低就一直更新它的值。如果股票进入“上升期”,就可以更新最高点
p
t
a
i
l
ptail
ptail的值,直到又下降,可以知道刚刚已经经过了最高点,可以在上一次卖出(典型的马后炮)。
if(phead>prices[i]&&ptail==-1) phead=prices[i];
else if(ptail==-1||ptail<prices[i]) ptail =prices[i];
else{
maxp+=ptail-phead;
phead=prices[i];
ptail=-1;
}
此处我使用了
p
t
a
i
l
=
−
1
ptail=-1
ptail=−1这样的初始值,来判定是否存在峰值,我们知道,一次结算之后,这个最高点已经用过了,需要重置。而
p
h
e
a
d
phead
phead是沿用了“判定后的第一个数”作为暂时的低点。
这时会出现一个问题,我们等到一条线下降了才知道最高点出现,但是如果最后一段一直是上涨阶段,直到“涨没了”(数组尽头),我们手里还有一支没有抛售的股票,怎么办呢?
于是,设置数组尽头的检查,如果最后成为了较高点,就更新
p
t
a
i
l
ptail
ptail并且进行结算,此处没有再重置
p
t
a
i
l
ptail
ptail的值是因为遍历已经结束了。
if(i==pricesSize-1&&prices[i]>phead)
{
if(prices[i]>ptail) ptail=prices[i];
maxp+=ptail-phead;
}
总的函数代码如下:
int maxProfit(int* prices, int pricesSize){
if(pricesSize==1) return 0;
//如果说股市是VVV形,只需要在最低点买,在最高点卖
int phead = prices[0],ptail=-1,maxp=0;
int i;
for(i=1;i<pricesSize;i++)
{
if(i==pricesSize-1&&prices[i]>phead)
{
if(prices[i]>ptail) ptail=prices[i];
maxp+=ptail-phead;
}
else{
if(phead>prices[i]&&ptail==-1) phead=prices[i];
else if(ptail==-1||ptail<prices[i]) ptail =prices[i];
else{
maxp+=ptail-phead;
phead=prices[i];
ptail=-1;
}
}
}
return maxp;
}
结果判定:
总结
我认为第二个方法比较好理解,但是它只是一个“特解”,所以又写下了动态规划的解法,可以帮助解决更多类型的问题。