本文记录笔者在解LeetCode上的Best Time to Buy and Sell Stock系列时的一些想法。该系列题目共有五道,它们背景相似,但因为条件限制不同,解决思路也不太相同。
Best Time to Buy and Sell Stock
https://leetcode.com/problems/best-time-to-buy-and-sell-stock/
题目大意是,给定一个关于每天股票价格的序列,只能买卖一次,求最大利润。
我首先想到的是求相邻两天的价格的差值,然后求这个关于差值的数列的最大子数列(53. Maximum Subarray)。
具体思路是DP,让dp[i]为以第i个元素为结尾的最大的子数列,那么有,
dp[i]={dp[i−1]+arr[i] if dp[i−1] is positivearr[i] otherwise
这可以在 O(n) 内完成。
class Solution(object):
def maxProfit(self, prices):
arr=[]
curr_max=0
last_max=0
for i in range(len(prices)-1):
curr=prices[i+1]-prices[i]
if last_max > 0:
last_max += curr
else:
last_max = curr
curr_max = max(curr_max,last_max)
return curr_max
后来看了其他人的答案,其实这可以用全局最优和局部最优的角度去考虑。如下面重写的代码中glo(bal)全局最优只有在小于loc(al)局部最优时才会被更新。当局部最优某天小于0,说明新的最低点发现了,这时候可以将局部最优置为0,说明局部最优的买入点要更新成这个新的最低点了。
这同样是 O(n) 的时间。
class Solution(object):
def maxProfit(self, prices):
glo=0
loc=0
for i in range(1,len(prices)):
delta = prices[i]-prices[i-1]
loc += delta
if loc < 0:
loc = 0
if loc > glo:
glo = loc
return glo
Best Time to Buy and Sell Stock II
https://leetcode.com/problems/best-time-to-buy-and-sell-stock-ii/
这也是给定了一个价格的序列,只不过允许任意次数的交易。
这个就很简单了,既然不限次数,那么每个相邻交易日,能赚的都赚了。
class Solution(object):
def maxProfit(self, prices):
""" :type prices: List[int] :rtype: int """
result=0
for i in range(len(prices)-1):
d=prices[i+1]-prices[i]
if d > 0:
result+=d
return result
Best Time to Buy and Sell Stock III
https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/
这道题的限制是,最多只能买卖两次。
我的做法是求所有[0, …, i]的子数列只买卖一次的大小,做法和Best Time to Buy and Sell Stock I 是一样的,结果放在pre[i]中。这个只需要扫描一遍整个数组即可。
再求所有[i, …, n-1]的子数列只买卖一次的大小,这个做法和上面的是相似的,只不过我们要从n-1开始,倒着扫描数组,并每次记录最糟糕的买卖情况(因为倒序买卖的最差情况,就对应着正序买卖的最好情况)。结果放在post[i]中。
上面两个过程都是 O(n) 时间的,那么我再求 maxi(pre[i]+post[i+1]) 。这个过程也是线性的。因此整个算法,总体时间复杂度是 O(n) 的。
class Solution(object):
def maxProfit(self, prices):
if len(prices) == 0 or len(prices) == 1:
return 0
pre = [0 for i in range(0, len(prices))]
post = [0 for i in range(0, len(prices))]
glo = 0
loc = 0
for i in range(1, len(prices)):
loc += prices[i] - prices[i - 1]
if loc < 0:
loc = 0
glo = max(glo, loc)
pre[i] = glo
glo = 0
loc = 0
for i in range(len(prices)-1,0,-1):
loc += prices[i-1]-prices[i]
if loc > 0:
loc = 0
glo = min(glo,loc)
post[i-1] = - glo
print(pre)
print(post)
curr = max(pre[len(prices)-1],post[0])
for i in range(1,len(prices)-1):
curr = max(curr,pre[i]+post[i+1])
return curr
Best Time to Buy and Sell Stock IV
https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iv/
这次的限制是,最多可以做k次买卖。
这个我就不会了,后来参考了一下网上的解法。
Best Time to Buy and Sell Stock III – LeetCode
http://blog.csdn.net/linhuanmars/article/details/23236995
这里介绍的解法就是DP,但状态的定义比较巧妙。总体思路也是一种局部最优、全局最优的方法。
global[i][j]表示前i天最多做j次买卖的全局最优值,local[i][j]表示前i天最多做j次买卖且第i天是需要卖出的。
它们的关系,有
global[i][j]=max(global[i−1][j],local[i][j])
这条式子是以第i天是否有卖出动作作为标准的。
local[i][j]=max(global[i−1][j−1]+max(diff,0),local[i−1][j]+diff)
其中diff是第i天和i-1天的差价。
这个算法时间复杂度是O(nk),如果令k=2可以直接获得第III的解法。
class Solution(object):
def maxProfit(self, k, prices):
if len(prices) == 0 or len(prices) == 1:
return 0
if k == 0:
return 0
if k >= len(prices):
acc = 0
for i in range(1,len(prices)):
if prices[i] - prices[i-1] > 0:
acc += prices[i]-prices[i-1]
return acc
glo = [[0, 0] for i in range(len(prices))]
loc = [[0, 0] for i in range(len(prices))]
# j == 1
best = 0
loc_maxima = 0
for i in range(1, len(prices)):
delta = prices[i] - prices[i - 1]
loc_maxima += delta
if loc_maxima < 0:
loc_maxima = 0
best = max(best, loc_maxima)
loc[i][1] = loc_maxima
glo[i][1] = best
# j > 1
for j in range(2, k + 1):
for i in range(1, len(prices)):
diff = prices[i] - prices[i - 1]
loc[i][j%2] = max(glo[i - 1][(j - 1)%2] + max(diff, 0), loc[i - 1][j%2] + diff)
glo[i][j%2] = max(loc[i][j%2], glo[i - 1][j%2])
print(glo)
print(loc)
return glo[len(prices) - 1][k%2]
Best Time to Buy and Sell Stock with Cooldown
https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-cooldow/
这道题的要求是,允许做任意多次交易,但在做卖出操作后的一天,是不能在做买入了,是为cooldown。
我一开始写了个贪心,但这是错的(我太天真了)。
后来我参考了这里,
Share my thinking process
https://discuss.leetcode.com/topic/30421/share-my-thinking-process
恰好该文作者也是该题目的作者。
解答用的是DP,定义了三种状态buy[],sell[]和rest[]。
先说明一下,所谓的交易序列就是类似于[buy, sell, rest, buy, rest …]的序列,其中元素分别代表该天做的动作。我们不用担心会出现在同一天出现buy和sell,因为这不能带来利润,且还带来额外的cooldown限制。
buy[i]的意思是,从第0天到第i天里面所有的以buy为结尾的交易序列中的最优解(最大利润),比如[buy,sell,buy],[rest,rest,buy]。注意[buy, rest, rest]也可以算这种序列。这里会让人产生疑惑,这和rest[i]不就冲突了吗,我们待会再看。
sell[i]和rest[i]意思是类似的。
那么这里就有状态转移,
buy[i]=max(rest[i−1]−price,buy[i−1])
sell[i]=max(buy[i−1]+price,sell[i−1])
rest[i]=max(sell[i−1],buy[i−1],rest[i−1])
buy[i]代表的最优解,可以从第i天是否买入来分类。换言之,比较从rest[i-1]代表的最优解减去i天价格,和第i天不做买入操作(rest)的结果。buy[i-1]代表的交易序列其实也是满足buy[i]的交易序列的要求的,因为序列最后一个操作还是buy(忽略尾带的 rest)。
sell[i]代表的最优解,同理,从第i天是否发生卖出操作分类。
rest[i]就随意多了,第i天肯定不能是buy或者sell,只能是rest。那么从0到i-1天无论什么序列,都是符合要求的。
针对前面rest可能带有的冲突、冗余的“属性”,作者后面又很敏锐地指出,rest[i]其实就等于sell[i-1]!这是因为rest[i]一定大于等于buy[i],而sell[i-1]又大于等于rest[i]。因此上面的第三个等式其实可以直接记作,rest[i]=sell[i-1]。同样的,第一个等式的rest[i-1]-price也可以改写成sell[i-2]-price了。
最后,只要设置好初始量,最后迭代到最后一天即可。而初始量,可以考虑
buy[-1] = - prices[0]
sell[-1] = 0
buy[0] = - prices[0]
sell [0] = 0
设置 -1 下标主要是因为buy[i]需要用到sell[i-2]。
class Solution(object):
def maxProfit(self, prices):
""" :type prices: List[int] :rtype: int """
if len(prices) == 0:
return 0
prev_buy = - prices[0]
prev_sell = 0
buy = -prices[0]
sell = 0
for i in range(1,len(prices)):
new_buy = max(buy,prev_sell-prices[i])
new_sell = max(sell,buy+prices[i])
prev_sell = sell
prev_buy = buy
sell = new_sell
buy = new_buy
return max(buy,sell)
#print(Solution().maxProfit([1,2,3,0,2]))