动态规划(二)【股票买卖】

141 阅读4分钟

买卖股票Ⅰ

题目描述

image-20210320212347204

解题思路

目标是要求得最大利润,可见包含最优子问题的特点,同时很显而易见,这些子问题是互斥的,所以可以往动态规划方面考虑。

状态转移方程:

可以先列出dp的关系和利润的关系方程,dp(n,status)=profitdp(n, status)=profit,n表示是第几天,status表示当天的持仓状态,有两种持仓和未持仓。状态转移方程则为:dp(n,0)=max(dp(n1,0),dp(n1,1)+prices[n])dp(n,0)=max(dp(n-1,0), dp(n-1,1)+prices[n])dp(n,1)=max(dp(n1,1),prices[n])dp(n,1)=max(dp(n-1,1),-prices[n])

代码实现

自顶向下,暴力递归

from typing import List


# 自顶向下
class Solution:
    def maxProfit(self, prices: List[int]) -> int:

        def dp(n, status):
            # base case
            if n == 0:
                if status == 0:
                    return 0
                else:
                    return -prices[0]
            if status == 0:  # 今日未持有股票
                return max(dp(n - 1, 0), dp(n - 1, 1) + prices[n])
            # 今日持有股票
            return max(dp(n - 1, 1), -prices[n])

        return dp(len(prices) - 1, 0)

执行结果:

image-20210321135151757

自顶向下,备忘录优化

from typing import List

# 自顶向下
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        memo = [[None] * 2 for _ in range(len(prices))]
        memo[0] = 0, -prices[0]

        def dp(n, status):
            # base case
            if memo[n][status] is not None:
                return memo[n][status]
            else:
                if status == 0:  # 今日未持有股票
                    ret = max(dp(n - 1, 0), dp(n - 1, 1) + prices[n])
                else:
                    # 今日持有股票
                    ret = max(dp(n - 1, 1), -prices[n])
                memo[n][status] = ret
                return ret

        return dp(len(prices) - 1, 0)

执行结果:

image-20210321163059165

可以看到,优化后虽然提交结果垫底,但是已经不超时了。

自底向上,有限迭代

除了从结果(也就是递归树的根节点)出发,我们还可以从base case出发,有限次的迭代求出最终的结果。

代码实现:

from typing import List


# 自底向上
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        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], -prices[i])
        return dp[n - 1][0]

执行结果:

image-20210321222246549

自底向上,空间优化

可以发现每次的运行结果都只跟前一次的有关,dp数组可以简化为两个变量

from typing import List


# 自底向上,空间优化
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n = len(prices)
        dp_i_0 = 0
        dp_i_1 = -prices[0]

        for i in range(1, n):
            dp_i_0, dp_i_1 = max(dp_i_0, dp_i_1 + prices[i]), max(dp_i_1, -prices[i])
        return dp_i_0

执行结果:

image-20210321223108555

股票买卖Ⅱ

题目描述

image-20210321223632299

简单来说就是在上题的基础上,不限制买卖股票的次数,股票交易次数为无限次。

解题思路

解题思路基本跟上题一致,不同的是状态转移方程中的dp(n,1)dp(n,1)改为dp(n,1)=max(dp(n1,1),dp(n1,0)prices[n])dp(n,1)=max(dp(n-1,1),dp(n-1,0)-prices[n])

代码实现

from typing import List


class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n = len(prices)
        dp_i_0 = 0
        dp_i_1 = -prices[0]

        for i in range(1, n):
            dp_i_0, dp_i_1 = max(dp_i_0, dp_i_1 + prices[i]), max(dp_i_1, dp_i_0-prices[i])
        return dp_i_0

提交结果

image-20210321224937443

股票买卖Ⅲ

题目描述

image-20210321225205185

解题思路

本题将买卖次数限定为两次,那么就需要将当天未持仓和当天持仓这两种状态再跟买卖的次数结合起来,所以一共有了四种状态:

  • 买过一次,当前为持仓状态
  • 卖过一次,当前为未持仓状态
  • 买过两次,当前为持仓状态
  • 卖过两次,当前为未持仓状态

状态转移方程就变为:

  • buy1=max(buy1,prices[i])buy1=max(buy1,-prices[i])
  • sell1=max(sell1,buy1+prices[i]sell1=max(sell1,buy1+prices[i]
  • buy2=max(buy2,sell1prices[i])buy2=max(buy2,sell1-prices[i])
  • sell2=max(sell2,buy2+prices[i])sell2=max(sell2,buy2+prices[i])

代码实现

from typing import List


class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n = len(prices)
        # 初始状态设置
        buy1 = buy2 = -prices[0]
        sell1 = sell2 = 0

        for i in range(1, n):
            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

提交结果

image-20210322112640640

股票买卖Ⅳ

题目描述

image-20210322112751889

解题思路

此题将最大交易次数作为变量k输入,那么状态转移方程就需要将k也考虑进去:

i天,经过了k次交易,当前持仓的收益

buy[i][k]=max(buy[i1][k],sell[i1][k1]prices[i])buy[i][k]=max(buy[i-1][k], sell[i-1][k-1]-prices[i])

i天,经过了k次交易,当前未持仓的收益 sell[i][k]=max(sell[i1][k],buy[i1][k]+prices[i])sell[i][k]=max(sell[i-1][k], buy[i-1][k]+prices[i])

同时需要注意考虑边界条件。

代码实现

from typing import List


class Solution:
    def maxProfit(self, k: int, prices: List[int]) -> int:
        n = len(prices)
        k = min(k, n // 2)
        if not prices or k == 0:
            return 0
        dp = [[[0] * k, [0] * k] for _ in range(n)]
        # 初始化,第一天买的都为-prices[0]
        dp[0][0] = [-prices[0]] * k
        for i in range(1, n):
            for j in range(0, k):
                # 如果是第一次买直接就是-prices[i]
                # buy_j=max(buy_j, sell_j-1 - price)
                dp[i][0][j] = max(dp[i - 1][0][j], -prices[i] if j == 0 else dp[i - 1][1][j - 1] - prices[i])
                # sell_j=max(sell_j,buy_j + price)
                dp[i][1][j] = max(dp[i - 1][1][j], dp[i - 1][0][j] + prices[i])
        return dp[n - 1][1][k - 1]

提交结果:

image-20210322124113820

股票买卖(冷冻期)

题目描述

image-20210322124230117

解题思路

有了冷冻期,那么买入时候的最大利润就是跟前两天的卖出利润有关,这里没有对买卖次数做限制,那么可以在股票买卖Ⅱ的基础上修改,状态转移方程就可以更改为:

dp[n][0]=max(dp[n1][0],dp[n2][1]+prices[i])dp[n][0]=max(dp[n-1][0], dp[n-2][1]+prices[i])

代码实现

from typing import List


class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n = len(prices)
        if n <= 1:
            return 0

        dp = [[0, 0] for _ in range(n)]
        dp[0] = [0, -prices[0]]
        dp[1][1] = max(dp[0][1], dp[0][0] - prices[1])

        for i in range(1, n):
            # 未持有
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i])
            if i > 1:
                # 持有
                dp[i][1] = max(dp[i - 1][1], dp[i - 2][0] - prices[i])
        return dp[n - 1][0]

提交结果:

image-20210322144548047

股票买卖(手续费)

题目描述

image-20210322144643359

解题思路

跟股票买卖Ⅱ基本一致,只是将fee也算到成本中去。

代码实现

class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        n = len(prices)
        dp_i_0 = 0
        dp_i_1 = -prices[0]

        for i in range(1, n):
            dp_i_0, dp_i_1 = max(dp_i_0, dp_i_1 + prices[i]-fee), max(dp_i_1, dp_i_0 - prices[i])
        return dp_i_0

提交结果

image-20210322144339159