【LeetCode】Best Time to Buy and Sell Stock IV

Best Time to Buy and Sell Stock IV

Say you have an array for which the ith element is the price of a given stock on day i.

Design an algorithm to find the maximum profit. You may complete at most k transactions.

Note:
You may not engage in multiple transactions at the same time (ie, you must sell the stock before you buy again).

Credits:
Special thanks to @Freezen for adding this problem and creating all test cases.

[Solution]

这题如果一开始不重视 k 的条件限制,很容易误以为是一道简单的贪心题。

大概会想,无非就是找出序列中 所有的上升区段,并用上升区段的最后一个数减第一个数(即此上升区段的跨度)作为其 value,最后再取 前 k 个最大的 values 求和得解。

典型的贪心思想,我一开始这样想写的代码如下:(注意:一次交易是指一次买入一次卖出操作,题目未说明清楚。)

 1 class Solution:
 2     # @return an integer as the maximum profit 
 3     def maxProfit(self, k, prices):
 4         if prices == []:
 5             return 0
 6         down = prices[0]
 7         up = prices[0]
 8         ansList = []
 9         for i in range(1, len(prices)):
10             if prices[i-1] < prices[i]:
11                 up = prices[i]
12             elif prices[i] < prices[i-1]:
13                 ansList.append(up - down)
14                 down = prices[i]
15                 up = prices[i]
16         ansList.append(up - down)
17         ansList.sort(reverse=True)
18         if k < len(ansList):
19             ans = sum(ansList[:k])
20         else:
21             ans = sum(ansList)
22         return ans
23             
24         

而且这贪心可以过:200 / 211 test cases passed.

但细想一下,正是因为有 k 的限制,其实 贪心 是不对的。

考虑这样一个样例:

 Input:   2, [1,2,4,2,5,7,2,4,9,0]

如果用上面那段代码得出的结果会是:12,显然是取了 3~5 和 6~8 这两个区段(list下标号)。

但实际上,因为 k 限制在 2,所以可以有更优的解:0~5 和 6~8: 6+7=13。

这样,我们就找了一个反例证明贪心的不正确性(其实也不难直观的去想到)。

实际上,由于 k 的限制,使得决策之间产生了联系,决定了贪心的错误性。

很自然的,做决策求最值,会想到用 DP (动态规划)来完成这道题。

再来思考问题的本质,可以发现这样的特点:

买入和卖出操作是交替执行的,而且卖出之前一定得先买入。

转化为这样的一个数学问题:

给定一个序列,从该序列中找出一个元素保持在原序列中顺序并且元素个数是偶数的子序列,使得其偶数项之和与奇数项之和的差最大。

而对于每一天来说,只有两种选择,即选择 操作 或者 不操作。而具体的操作是买入还是卖出得由当前已经执行的操作数是奇偶性来决定。

所以我们可以从天数递增来地推上去直到第n天。

如果用 dp[j] 表示 完成第j次交易时 (由 j-1 转移到 j 必需伴随着一次操作)的最大收益,则状态转移方程可以写为:

dp[j] = max{dp[j], dp[j-1] + prices[i] * (j%2)?-1:1}

外循环 天数 i 从 0 递推到 size (第 j 次交易不一定发生在哪一天),内循环 j 从 1 递推到当前允许的最大操作数:min{2*k, i+1} (i 是 prices 中的 第 i+1 个元素)。

初始条件:dp[0] = 0,dp中的其他值为无穷小(None比任何数值都小)。

另外,有一个优化,当 2*k >= szie 时,k 的限制已经形同虚设了,操作数限制已经可以覆盖到每一天都允许操作,这种情况下还用上述DP效率太低,可以改为没有k限制的贪心版本(把所有上升区段跨度求和即为解)。

由于有了上述优化,所以用DP的情况一定是 2*k < size的情况,这种情况下 用完 2*k 次操作数限制所得的结果一定等于最优解(虽然不一定实际上进行2*k次操作,因为此时实际上放宽了题目限制条件,在同一个 i 值,dp[j]可能把这个 i 买入使得当前更优(因为初始值为无穷小),而到了dp[j+1]就把这个 i 卖出使得当前更优,实际上就相当于不操作,但记录的操作数 j 还是增加了2,所以实际上此时dp[j]表示的准确的实际含义是 最多完成j次交易时(实际可能不在j次时是最优),放大到全局即为 2*k 处最优。而又因为 i 是递推上去到 n 的,所以总会有找到 正确的最后一次交易使得在第 i 天dp[2*k]最大,而 i 再增加,操作数已经不能增加了(已经到了最大的限制)),所以最后的结果为 dp[2*k]。

最后AC代码:

 1 class Solution:
 2     # @return an integer as the maximum profit 
 3     def maxProfit(self, k, prices):
 4         size = len(prices)
 5         if k*2 >= size:
 6             return self.noKLimit(size, prices)
 7         dp = [None] * (2*k+1)
 8         dp[0] = 0
 9         for i in range(size):
10             for j in range(1, min(2*k, i+1)+1):
11                 dp[j] = max(dp[j], dp[j-1]+prices[i]*[1, -1][j%2])
12         return dp[2*k]
13             
14     def noKLimit(self, size, prices):
15         sum = 0
16         for i in range(size - 1):
17             if prices[i+1]>prices[i]:
18                 sum += prices[i+1] - prices[i]
19         return sum
20         

注:一开始做的时候还没发现原来这是一个系列题,这是目前该系列最后一个(第四题),应该递进的来做可能比较简单。

参考:[LeetCode]Best Time to Buy and Sell Stock IV

原文地址:https://www.cnblogs.com/maples7/p/4350047.html