声明:本人首先接触的是使用三维DP模型解决股票问题,但是不易理解。于是本人有幸阅读了LeetCode 股票问题的一种通用解法,并获取原作者同意后,部分内容摘录于原文。意在记录作者提供的通用递归思路和自己做题思路历程
再次声明:本文记录的是容易理解的、非最优解题思路
LeetCode中的股票问题包括:
121 买卖股票的最佳时机
122 买卖股票的最佳时机 II
123 买卖股票的最佳时机 III
188 买卖股票的最佳时机 IV
309 最佳买卖股票时机含冷冻期
714 买卖股票的最佳时机含手续费
对于这 6 道题目,都有一些大佬给出了时间、空间复杂度较优的解法,但是却存在两个问题:
- 未能将这 6 道题目联系起来
- 代码让小白看了容易怀疑人生
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if not prices:
return 0
buy1, sell1, buy2, sell2 = -prices[0], 0, -prices[0], 0
for i in range(1,len(prices)):
buy1 = max(buy1,-prices[i])
sell1 = max(sell1,buy1 + prices[i])
buy2 = max(buy2,sell1 - prices[i])
sell2 = max(sell2,buy2 + prices[i])
return sell2
如上代码是第三个问题的执行最优代码。然而这么优秀的代码让大家读不懂,读懂了也不一定自己能敲出来,能敲出来也是 6 道题分开记忆。
于是乎,原文作者便带领大家学习了一个容易理解的解决股票问题的通用框架。
我们慢慢来推出这个次优解框架:
1, 买卖股票的最佳时机
对于此问题,面试时,如果一时间没想起来最优解法,推荐先使用暴力法,然后在写暴力法代码时,思考更优解。方便理解,我们直接暴力!
for i: 0~n # 第 i 天买入
for j: i+1~n # 第 j 天卖出, j > i
maxPro = max(maxPro, prices[j] - prices[i])
在暴力解法中,我们是枚举了所有可能的 buy_day,然后遍历买入后的每一天作为 sell_day,寻找 prices[sell_day] 最大的那天,这样在 sell_day 卖出才能获得较大利润。
如果反过来,我们固定 sell_day, 前向穷举 buy_day, 寻找 prices[buy_day] 最小的那天,也能达到同样效果。并且还能减少一个 for 循环。
这样,代码就可以这样写:
maxPro = 0
minPri = prices[0]
for sell_day in range(1, len(prices)):
minPri = min(minPri, prices[sell_day])
maxPro = max(maxPro, prices[sell_day] - minPri)
有了上述代码后,便可以此为框架解决另外 5 题
2, 买卖股票的最佳时机 II
此题允许无限次交易,我们按照上一题的暴力思想穷举所有情况:
for buy_day in range(len(prices)):
for sell_day in range(buy_day+1, len(prices)):
if sell_day == 1: for ... # 第一次交易时间是 buy_day, sell_day = 0, 1,枚举后面的不限交易次数可获的最大利润
if sell_day == 2: for ... # 第一次交易时间是 buy_day, sell_day = 0, 2,枚举后面的不限交易次数可获的最大利润
...
if sell_day == len(prices) -1: for ...
发现这是无穷 for 循环情况,这是使用递归的强烈暗示。
上一题的框架是解决一次交易可得的最大收益,现在的问题是,进行一次交易后,下一次应该什么时候交易。分析可得,这个问题与原问题有相同结构,规模减小,是典型的递归场景。
import pysnooper
@pysnooper.snoop()
def maxProfit(prices):
maxPro = 0
for buy in range(len(prices)):
for sell in range(buy+1, len(prices)):
maxPro = max(maxPro, maxProfit(prices[sell+1:]) + prices[sell] - prices[buy])
return maxPro
maxProfit([5,1,3,])
Starting var:.. prices = [5, 1, 3]
17:25:40.928617 call 3 def maxProfit(prices):
17:25:40.928617 line 4 maxPro = 0
New var:....... maxPro = 0
17:25:40.928617 line 5 for buy in range(len(prices)):
New var:....... buy = 0
17:25:40.928617 line 6 for sell in range(buy+1, len(prices)):
New var:....... sell = 1
17:25:40.929116 line 7 maxPro = max(maxPro, maxProfit(prices[sell+1:]) + prices[sell] - prices[buy])
Starting var:.. prices = [3]
17:25:40.929116 call 3 def maxProfit(prices):
17:25:40.929116 line 4 maxPro = 0
New var:....... maxPro = 0
17:25:40.929116 line 5 for buy in range(len(prices)):
New var:....... buy = 0
17:25:40.929116 line 6 for sell in range(buy+1, len(prices)):
17:25:40.929116 line 5 for buy in range(len(prices)):
17:25:40.929614 line 8 return maxPro
17:25:40.929614 return 8 return maxPro
Return value:.. 0
17:25:40.929614 line 6 for sell in range(buy+1, len(prices)):
Modified var:.. sell = 2
17:25:40.929614 line 7 maxPro = max(maxPro, maxProfit(prices[sell+1:]) + prices[sell] - prices[buy])
Starting var:.. prices = []
17:25:40.930113 call 3 def maxProfit(prices):
17:25:40.930113 line 4 maxPro = 0
New var:....... maxPro = 0
17:25:40.930113 line 5 for buy in range(len(prices)):
17:25:40.930113 line 8 return maxPro
17:25:40.930113 return 8 return maxPro
Return value:.. 0
17:25:40.930113 line 6 for sell in range(buy+1, len(prices)):
17:25:40.930113 line 5 for buy in range(len(prices)):
Modified var:.. buy = 1
17:25:40.930612 line 6 for sell in range(buy+1, len(prices)):
17:25:40.930612 line 7 maxPro = max(maxPro, maxProfit(prices[sell+1:]) + prices[sell] - prices[buy])
Starting var:.. prices = []
17:25:40.930612 call 3 def maxProfit(prices):
17:25:40.930612 line 4 maxPro = 0
New var:....... maxPro = 0
17:25:40.930612 line 5 for buy in range(len(prices)):
17:25:40.931111 line 8 return maxPro
17:25:40.931111 return 8 return maxPro
Return value:.. 0
Modified var:.. maxPro = 2
17:25:40.931111 line 6 for sell in range(buy+1, len(prices)):
17:25:40.931111 line 5 for buy in range(len(prices)):
Modified var:.. buy = 2
17:25:40.931111 line 6 for sell in range(buy+1, len(prices)):
17:25:40.931111 line 5 for buy in range(len(prices)):
17:25:40.931610 line 8 return maxPro
17:25:40.931610 return 8 return maxPro
Return value:.. 2
2
由代码执行输出可见,此递归版本代码就是执行一次交易后,递归下一次交易情况
现在可以根据上一题减少 for 循环的优化思想,再创建备忘录,即 递归 + 记忆化 进行优化框架
这里,就有点 DP 的味道了,我们将 递归 + 记忆化 进行递推的话,就可以得到 DP
def maxProfit(prices):
def dp(buy):
if buy >= len(prices): return 0
if buy in cache: return cache[buy]
maxPro = 0
minPri = prices[buy]
for sell in range(buy+1, len(prices)):
minPri = min(minPri, prices[sell])
maxPro = max(maxPro, dp(sell+1) + prices[sell] - minPri)
cache[buy] = maxPro
return maxPro
cache = {} # 这里使用字典,写代码方便但是空间复杂度比列表高;又但是,都说了是次优解,当然不管这细节
return dp(0)
上述代码易懂但存在缺点,时间复杂度O(n^2),会在最后一个长用例超时。
该问题的最优解是贪心算法。
3,4,买卖股票的最佳时机 III & 买卖股票的最佳时机 IV
这两题限制了最大交易次数,第 IV 题是第 II 题的推广,我们来搞定第 IV 题即可。
我们利用前面的递归框架,并增加交易次数的约束即可
def maxProfit(k, prices):
def dp(buy, k):
if buy >= len(prices) or k == 0: return 0 # 边界条件
if (buy, k) in cache: return cache[(buy, k)]
maxPro = 0
minPri = prices[buy]
for sell in range(buy+1, len(prices)):
minPri = min(minPri, prices[sell])
maxPro = max(maxPro, dp(buy+1, k-1) + prices[sell] - minPri) # 增加交易次数约束
cache[(buy, k)] = maxPro
return maxPro
cache = {}
return dp(0, k)
时间复杂度 O(k*n^2),同样会在最后一个测试用例超时
此题不限制交易次数,但是存在交易冷却时间,即每次交易结束(卖出股票)后,交易操作进入一天时间的冷却,只需修改交易后的买入时间即可
def maxProfit(prices):
def dp(buy):
if buy >= len(prices): return 0 # 边界条件
if buy in cache: return cache[buy]
maxPro = 0
minPri = prices[buy]
for sell in range(buy+1, len(prices)):
minPri = min(minPri, prices[sell])
maxPro = max(maxPro, dp(sell + 2) + prices[sell] - minPri) # 修改交易后的买入时间
cache[buy] = maxPro
return maxPro
cache = {}
return dp(0)
时间复杂度 O(n^2),在大规模测试用例时,会超时。但是套模版的方法简单直接!
此题,不限交易次数,存在交易手续费,套进框架,每次卖出时减去手续费即可
def maxProfit(prices, fee):
def dp(buy):
if buy >= len(prices): return 0
if buy in cache: return cache[buy]
maxPro = 0
minPri = prices[buy]
for sell in range(buy+1, len(prices)):
minPri = min(minPri, prices[sell])
maxPro = max(maxPro, dp(sell + 2) + prices[sell] - minPri - fee)
cache[buy] = maxPro
return maxPro
cache = {}
return dp(0)
文章终了,小结一下。
通过分析第一题,得到股票问题中一次交易的解决办法
maxPro = 0
minPri = prices[0]
for sell_day in range(1, len(prices)):
minPri = min(minPri, prices[sell_day])
maxPro = max(maxPro, prices[sell_day] - minPri)
对于不限制交易次数问题,我们采用递归 + 记忆化方法得出模版
def maxProfit(prices):
def dp(buy):
if buy >= len(prices): return 0
if buy in cache: return cache[buy]
maxPro = 0
minPri = prices[buy]
for sell in range(buy+1, len(prices)):
minPri = min(minPri, prices[sell])
maxPro = max(maxPro, dp(sell+1) + prices[sell] - minPri)
cache[buy] = maxPro
return maxPro
cache = {}
return dp(0)
对于其他问题,我们只需对模版进行简单修改即可通过绝大多数案例。
此模版在面试或笔试中,都十分有优势!
最后继续推荐阅读原文LeetCode 股票问题的一种通用解法–labuladong