LeetCode刷题 --股票篇
好久没得写刷题的博客了,正好最近牛市,记录几个股票相关的题,其实leetcode上相关内容不少,后面几道困难的有空再整理进来吧。
121. 买股票的最佳时机 I
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。
注意:你不能在买入股票前卖出股票。
示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
这道题其实以前有做过,正好笔者最近在玩Go,就尝试着用Go写了一遍
package leetcode_121 import ( "math" ) func maxProfit(prices []int) int { var result int = 0 var min int = math.MaxInt64 for i:=0; i < len(prices); i++ { if prices[i] < min { min = prices[i] } else if prices[i] - min > result { result = prices[i] - min } } return result }
其实题目可以简化成在一个数组中求下标a,b(a<b)使得a,b之间的差值最大.(a点买入股票,b点卖出股票).因此不妨借助一个中间变量min,代表股票的最低点,如果我们发现了最低点,那么就更新买入的时机(只能买卖一次)。否则就判断当天卖出的利润是否超过之前的利润。如此可以在一遍循环内完成计算。
122.买卖股票的最佳时机 II
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
示例 2:
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
提示:
1 <= prices.length <= 3 * 10 ^ 4
0 <= prices[i] <= 10 ^ 4
同样这道题之前笔者有刷到过,用Go再来一遍:
package leetcode_122 //这么一想这个思路好像有点意思,只要遇到后面的数值大于前面的,就求差值并入结果中。 func maxProfit(prices []int) int { if len(prices) == 0 { return 0 } var result int = 0 for i:=1; i < len(prices); i++ { if prices[i] - prices[i-1] > 0 { result += prices[i] - prices[i-1] } } return result }
与121的差异在于,这次我们可以买卖多次股票,也就是说即使某次交易不是在最低点开始,也不是在最高点结束,只要他们的差值是正的并且不影响其他的交易操作,我们就应该进行这一次的计算。反应成代码其实可以说是如果两个相邻的数满足后面的数字较大,我们就可以把他们的差值计算道最后的总收益当中。
可能会对上面的做法产生疑惑,因为题目有要求不能在同一时间参与多此交易。但对于类似于[4,5,6]这样的数据,虽然我们的计算过程中"5"参与了两次计算,但整体来说其实是可以看作一次交易的。因为我们计算的是每次的差值的和,可以相当于直接计算了4~6这次操作的差值,而“5”只是辅助计算的工具。
309. 最佳买入时机含冷冻期
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
- 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
- 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
示例:
输入: [1,2,3,0,2] 输出: 3 解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
分析题目,笔者第一思路其实是可以尝试使用动态规划的思想来解决问题。其实可以把题目转化成在股票交易周期内每天可以获得的最大总利润,而最后的结果就是最后一天的最大利润。最后一天的最大利润显然是受前一天的股票买入卖出状态所影响的。但再深挖下去,笔者没有想出一个好的办法来处理不同的状态转移。
(因为涉及到前一天是否已卖出股票,如果前一天持有股票,则当天可选择继续持有或卖出。如果前一天卖出股票,则当天是冷冻期,不能交易。如果前一天没有股票,则新一天可以选择不买如或买入股票。)
来看看官方的解法吧:
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/solution/zui-jia-mai-mai-gu-piao-shi-ji-han-leng-dong-qi-4/
来源:力扣(LeetCode)
方法一:动态规划
思路与算法
我们用 f[i]f[i] 表示第 ii 天结束之后的「累计最大收益」。根据题目描述,由于我们最多只能同时买入(持有)一支股票,并且卖出股票后有冷冻期的限制,因此我们会有三种不同的状态:
我们目前持有一支股票,对应的「累计最大收益」记为 f[i][0]f[i][0];
我们目前不持有任何股票,并且处于冷冻期中,对应的「累计最大收益」记为 f[i][1]f[i][1];
我们目前不持有任何股票,并且不处于冷冻期中,对应的「累计最大收益」记为 f[i][2]f[i][2]。
这里的「处于冷冻期」指的是在第 ii 天结束之后的状态。也就是说:如果第 ii 天结束之后处于冷冻期,那么第 i+1i+1 天无法买入股票。
如何进行状态转移呢?在第 ii 天时,我们可以在不违反规则的前提下进行「买入」或者「卖出」操作,此时第 ii 天的状态会从第 i-1i−1 天的状态转移而来;我们也可以不进行任何操作,此时第 ii 天的状态就等同于第 i-1i−1 天的状态。那么我们分别对这三种状态进行分析:
对于 f[i][0]f[i][0],我们目前持有的这一支股票可以是在第 i-1i−1 天就已经持有的,对应的状态为 f[i-1][0]f[i−1][0];或者是第 ii 天买入的,那么第 i-1i−1 天就不能持有股票并且不处于冷冻期中,对应的状态为 f[i-1][2]f[i−1][2] 加上买入股票的负收益 {\it prices}[i]prices[i]。因此状态转移方程为:
f[i][0] = \max(f[i-1][0], f[i-1][2] - {\it prices}[i])
f[i][0]=max(f[i−1][0],f[i−1][2]−prices[i])
对于 f[i][1]f[i][1],我们在第 ii 天结束之后处于冷冻期的原因是在当天卖出了股票,那么说明在第 i-1i−1 天时我们必须持有一支股票,对应的状态为 f[i-1][0]f[i−1][0] 加上卖出股票的正收益 {\it prices}[i]prices[i]。因此状态转移方程为:
f[i][1] = f[i-1][0] + {\it prices}[i]
f[i][1]=f[i−1][0]+prices[i]
对于 f[i][2]f[i][2],我们在第 ii 天结束之后不持有任何股票并且不处于冷冻期,说明当天没有进行任何操作,即第 i-1i−1 天时不持有任何股票:如果处于冷冻期,对应的状态为 f[i-1][1]f[i−1][1];如果不处于冷冻期,对应的状态为 f[i-1][2]f[i−1][2]。因此状态转移方程为:
f[i][2] = \max(f[i-1][1], f[i-1][2])
f[i][2]=max(f[i−1][1],f[i−1][2])
这样我们就得到了所有的状态转移方程。如果一共有 nn 天,那么最终的答案即为:
\max(f[n-1][0], f[n-1][1], f[n-1][2])
max(f[n−1][0],f[n−1][1],f[n−1][2])
注意到如果在最后一天(第 n-1n−1 天)结束之后,手上仍然持有股票,那么显然是没有任何意义的。因此更加精确地,最终的答案实际上是 f[n-1][1]f[n−1][1] 和 f[n-1][2]f[n−1][2] 中的较大值,即:
\max(f[n-1][1], f[n-1][2])
max(f[n−1][1],f[n−1][2])
细节
我们可以将第 00 天的情况作为动态规划中的边界条件:
\begin{cases} f[0][0] &= -{\it prices}[0] \\ f[0][1] &= 0 \\ f[0][2] &= 0 \end{cases}
⎩
⎪
⎨
⎪
⎧
f[0][0]
f[0][1]
f[0][2]
=−prices[0]
=0
=0
在第 00 天时,如果持有股票,那么只能是在第 00 天买入的,对应负收益 -{\it prices}[0]−prices[0];如果不持有股票,那么收益为零。注意到第 00 天实际上是不存在处于冷冻期的情况的,但我们仍然可以将对应的状态 f[0][1]f[0][1] 置为零,这其中的原因留给读者进行思考。
这样我们就可以从第 11 天开始,根据上面的状态转移方程进行进行动态规划,直到计算出第 n-1n−1 天的结果。
C#实现:
public class Solution { public int MaxProfit(int[] prices) { if (prices.Length == 0) return 0; int[,] dp = new int[prices.Length,3]; dp[0, 0] = -prices[0]; for (int i = 1; i < prices.Length; i++) { dp[i, 0] = Math.Max(dp[i - 1, 0], dp[i - 1, 2] - prices[i]); dp[i, 1] = dp[i - 1, 0] + prices[i]; dp[i, 2] = Math.Max(dp[i - 1, 1], dp[i - 1, 2]); } return Math.Max(dp[prices.Length - 1, 1], dp[prices.Length - 1, 2]); } }
Go实现:
func maxProfit(prices []int) int { if len(prices) == 0 { return 0 } var dp0 = -prices[0] var dp1 = 0 var dp2 = 0 for i:= 1; i<len(prices); i++{ var newdp0 int if dp0 > (dp2 - prices[i]){ newdp0 = dp0 } else { newdp0 = dp2 - prices[i] } newdp1 := dp0 + prices[i] var newdp2 int if dp1 > dp2{ newdp2 = dp1 } else { newdp2 = dp2 } dp0 = newdp0 dp1 = newdp1 dp2 = newdp2 } if dp1 > dp2 { return dp1 } else { return dp2 } }