day41 动态规划part08 买卖股票-上

83 阅读6分钟

鸽了好久了orz,有一阵子没跟上,后面越欠越多直接开摆了。 不能这样下去啦,接下来争取每天补1-2天,把剩下一半动态规划和图论刷完!尽量周末时候打周赛~

121. 买卖股票的最佳时机

文章讲解

思路1:贪心

贪心:取左边最小,右边最大的两天
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        low = float("inf") # 左侧最小
        result = 0 # 最大利润
        for i in range(len(prices)):
            low = min(low, prices[i])
            result = max(result, prices[i] - low)
        return result

思路2:动态规划

动态规划,构建n*2的dp table,dp[i][0]表示第i天持有股票的最大现金,dp[i][1]是不持有股票的最大现金
对于dp[i][0],有两种状态转移过来,1 是前一天i-1持有到今天dp[i-1][0],另一种是前一天不持有,持有了今天的股票-prices[i]
对于dp[i][1],有两种状态转移过来,1 前一天不持有dp[i-1][1],另一种卖掉了i-1天持有的股票,dp[i-1][0]+prices[i]
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices: return 0

        n = len(prices)
        dp = [[0,0] for _ in range(n)]
        dp[0][0] = -prices[0]
        for i in range(1, n):
            dp[i][0] = max(dp[i-1][0], -prices[i]) # 持有
            dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]) # 不持有
        return dp[n-1][1] # 最后一天不持有

思路3:动态规划-状态压缩

i只和i-1有关系,保留2*2的状态矩阵。遇到dp时候,i%2
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices: return 0
        n = len(prices)
        dp = [[0,0] for _ in range(2)]
        dp[0][0] = -prices[0]
        for i in range(1, n):
            # 遇到dp需要把i%2
            dp[i%2][0] = max(dp[(i-1)%2][0], -prices[i]) # 持有
            dp[i%2][1] = max(dp[(i-1)%2][1], dp[(i-1)%2][0] + prices[i]) # 不持有
        return dp[-1][1] # 最后一天不持有

122. 买卖股票的最佳时机 II

文章讲解

多次买入股票,一次只能有一笔交易。

思路:


本题与121题“买卖股票的最佳时机”的区别在于,本题中的股票可以买卖多次(但要注意只有一只股票,所以在再次购买之前必须先出售之前的股票)。在动态规划的五个步骤中,这个区别主要体现在递推公式上,其他步骤与121题相同。因此,我们将重点讲解递推公式。

这里重新说明一下dp数组的含义:

-   dp[i][0]表示第i天持有股票所得的现金。
-   dp[i][1]表示第i天不持有股票所得的最多现金。

如果第i天持有股票(dp[i][0]),那么可以由以下两个状态推导出来:

1.  第i-1天就持有股票,保持现状,所得现金就是昨天持有股票的所得现金,即:dp[i - 1][0]1.  第i天买入股票,所得现金就是昨天不持有股票的所得现金减去今天的股票价格,即:dp[i - 1][1] - prices[i]。

注意,这里与121题“买卖股票的最佳时机”唯一不同的地方是,在推导dp[i][0]时,第i天买入股票的情况。在121题中,因为股票全程只能买卖一次,所以如果买入股票,第i天持有股票(dp[i][0])一定就是 -prices[i]。而本题中,因为一只股票可以买卖多次,所以当第i天买入股票时,所持有的现金可能有之前买卖过的利润。因此,第i天持有股票(dp[i][0]),如果是第i天买入股票,所得现金就是昨天不持有股票的所得现金减去今天的股票价格,即:dp[i - 1][1] - prices[i]。

再来看看如果第i天不持有股票(dp[i][1])的情况,同样可以由以下两个状态推导出来:

1.  第i-1天就不持有股票,保持现状,所得现金就是昨天不持有股票的所得现金,即:dp[i - 1][1]1.  第i天卖出股票,所得现金就是按照今天股票价格卖出后所得现金,即:prices[i] + dp[i - 1][0]。

注意,这里与121题“买卖股票的最佳时机”的逻辑相同,卖出股票收获利润(可能是负值)是理所当然的。

以下是代码实现:(注意代码中的注释,标记了与121题“买卖股票的最佳时机”唯一不同的地方)
'''
思路:动态规划,dp n*2, dp[i][0]表示持有的最大现金,dp[i][1]表示不持有股票的最大现金,和买卖一次股票的区别在于,第i天购买股票时,需要考虑i-1天不持有的最大现金 dp[i-1][1] - prices[i]
'''
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices: return 0
        n = len(prices)
        dp = [[0,0] for _ in range(n)]
        
        dp[0][0] = -prices[0]
        for i in range(1, n):
            dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]) # 持有
            dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]) # 不持有

        return dp[-1][1]        

思路2:滚动数组

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices: return 0
        n = len(prices)
        dp = [[0,0] for _ in range(2)]
        
        dp[0][0] = -prices[0]
        dp[0][1] = 0

        for i in range(1, n):
            dp[i%2][0] = max(dp[(i-1)%2][0], dp[(i-1)%2][1] - prices[i]) # 持有
            dp[i%2][1] = max(dp[(i-1)%2][1], dp[(i-1)%2][0] + prices[i]) # 不持有

        return dp[(n-1)%2][1]        # 注意!,不是-1,如果prices是偶数就是-1,否则是0 

注意,滚动数组最终返回的不是dp[-1][1]。

123. 买卖股票的最佳时机 III

文章讲解

前面两题一个是一组,一个是多组,现在又限制为两次交易了。

思路:

交易两次,状态要double
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        # 买卖两次,需要5种状态,[不操作 第一次持有 第一次不持有 第二次持有 第二次不持有 ]
        if not prices:
            return 0
        n = len(prices)

        dp = [[0]* 5 for _ in range(n)]
        dp[0][1] = -prices[0]
        dp[0][3] = -prices[0]

        for i in range(1, n):
            dp[i][0] = dp[i-1][0] # 都是0吧
            dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i]) # 持有 扣钱
            dp[i][2] = max(dp[i-1][2], dp[i-1][1] + prices[i]) # 不持有 赚钱
            dp[i][3] = max(dp[i-1][3], dp[i-1][2] - prices[i]) # 持有 扣钱
            dp[i][4] = max(dp[i-1][4], dp[i-1][3] + prices[i]) # 不持有 赚钱
        return dp[-1][4] # 注意:1 dp[-1][4]不是3,五种状态; 2 注意符号-+-+

滚动数组:

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        # 买卖两次,需要5种状态,[不操作 第一次持有 第一次不持有 第二次持有 第二次不持有 ]
        if not prices:
            return 0
        n = len(prices)

        dp = [0]* 5
        dp[1] = -prices[0]
        dp[3] = -prices[0]

        for i in range(1, n):
            dp[1] = max(dp[1], dp[0] - prices[i]) # 持有 扣钱
            dp[2] = max(dp[2], dp[1] + prices[i]) # 不持有 赚钱
            dp[3] = max(dp[3], dp[2] - prices[i]) # 持有 扣钱
            dp[4] = max(dp[4], dp[3] + prices[i]) # 不持有 赚钱
        return dp[4] 

加油!! 坚持1天!!