Cancel

LeetCode Best Time to Buy and Sell Stock

Other

·

October 01, 2023

0121. Best Time to Buy and Sell Stock

Easy

单次交易

def maxProfit(self, prices: List[int]) -> int:
    n = len(prices)
    ans = 0
    h = 0

    for i in range(n-1, -1, -1):
        if h < prices[i]:
            h = prices[i]
        else:
            ans = max(ans, h - prices[i])

            return ans

0122. Best Time to Buy and Sell Stock II

Medium

不限次数交易

def maxProfit(self, prices: List[int]) -> int:
    ans = 0

    for i in range(1, len(prices)):
        ans += max(0, prices[i] - prices[i-1])

        return ans

0123. Best Time to Buy and Sell Stock III

Hard

限制两次交易

破题

题目的复杂程度终于提升到了有点儿意思的程度,这里的关键是搞清楚“最多两次”交易的限制条件到底对我们的模型意味着什么?

可以认为,做两次交易的收益总是不差于一次交易,只要在计算模型上把一次交易视为两次交易的边缘值。

解①两遍扫描

把价格表分为两部分,左边用于第一次交易,右边用于第二次交易,这样只需要检查每个位置的两次交易之和,求一个最大值即可。可以让左边分割的串从空串开始,这样就包含了一次交易的情况。

每次分割的左边串的最大收益可以依靠前面 0121. Best Time to Buy and Sell Stock 来计算,而分割的右边串则可以预先运用同样的算法反向扫描,保存个位置的计算结果,在从左开始扫描的时候查询表即可。

def solve(prices: List[int]) -> int:
    n = len(prices)

    dp = [0] * (n+1)
    hi = prices[-1]

    for i in range(n-2, -1, -1):
        if prices[i+1] > hi:
            hi = prices[i+1]

        if hi - prices[i] > dp[i+1]:
            dp[i] = hi - prices[i]
        else:
            dp[i] = dp[i+1]


    ans = dp[0]
    lo = prices[0]

    for i in range(1, n):
        if prices[i-1] < lo:
            lo = prices[i-1]

        if prices[i] - lo + dp[i+1] > ans:
            ans = prices[i] - lo + dp[i+1]

    return ans

运行时间:760 ms (beats 98.06%),内存占用:30.5 MB (beats 72.56%, ~100%)

解②规范解

在上一个解的时候,我们通过组合单次求解的方法来计算两次交易得到的最大收益,而这个单次求解的方法好像扫描一个个位置就可以进行该位置的更新,(因为当天交易收益总是零,因此是否允许当天交易都不重要),这暗示我们很可能有一个扫描每个位置就可以计算出两次交易的最大收益的模型。

这才引出对股票买卖问题的统一模型:

累积计算每次买或卖的收益,买的收益是 $-\text{price}$ ,卖的收益是买的收益加上 $\text{price}$ ,下次交易的收益基础是之前的收益。

对于两次买卖就有:

\[\begin{align} \text{buy}_1 &= \max(\text{buy}_1',\text{-price})\\ \text{sell}_1 &= \max(\text{buy}_1',\text{price})\\ \text{buy}_2 &= \max(\text{sell}_1',\text{-price})\\ \text{sell}_2 &= \max(\text{buy}_2',\text{price})\\ \end{align}\]

也就是:$\text{buy}_1 \rightarrow \text{sell}_1 \rightarrow \text{buy}_2 \rightarrow \text{sell}_2$

有必要解释下,按照自然流程,两次交易至少应该是第一天 $\text{buy}_1$ ,第二天 $\text{sell}_1$ ,第三天 $\text{buy}_2$,第四天 $\text{sell}_2$,是一个 $4$ 天的窗口,但是这需要额外检查下数据长度,而把这些操作压缩到一天,并不影响结果,而且可以适应长度不足 $4$ 的情况。1

def solve(prices: List[int]) -> int:
    buy1 = -prices[0]
    sell1 = 0  # prices[0]+buy1
    buy2 = -prices[0]
    sell2 = 0  # prices[0]+buy2

    for price in prices[1:]:
        if buy1 < -price:
            buy1 = -price
        elif sell1 < buy1+price:
            sell1 = buy1+price
        if buy2 < sell1-price:
            buy2 = sell1-price
        elif sell2 < buy2+price:
            sell2 = buy2+price

    return sell2

运行时间:670 ms (beats 99.76%),内存占用:30.5 MB (beats 88.72%, ~100%) 。

这个统一模型在一遍扫描就解决了问题,并且省掉了前面的 $O(n)$ 的内存占用2 。

0188. Best Time to Buy and Sell Stock IV

Hard

上面问题的一般化,不是限制两次交易,而是给定的参数 $k$ 次。

def solve(k: int, prices: List[int]) -> int:
    buy = [-prices[0]] * k
    sell = [0] * (k+1)  # buy[i] - sell[i+1]

    for price in prices:
        for i in range(k):
            if buy[i] < sell[i]-price:
                buy[i] = sell[i]-price
            elif sell[i+1] < buy[i]+price:
                sell[i+1] = buy[i]+price

    return sell[-1]

运行时间:59 ms (Beats 99.16%, ~100%),内存占用:16.36MB (Beats 82.07%,~100%)。

0309. Best Time to Buy and Sell Stock with Cooldown

Medium

不限次交易,有冷冻期

破题

购买冷冻期机制的引入改变了整个的模型,使得我们不能像前面的那个无限次数交易那样简单统计所有增量,就可以得到答案。

如果遵循股票买卖整个系列的顺序来做题,题解可能格外困难,因为很容易受到前面解题思路的影响,而重构整个模型并没有那么直观,因此我认为应该把这道题标记为 Hard 难度。

因此,让我们先从最简单地、最基本地方法开始:一个基于递归,使用记忆化优化的,穷举所有可能的解决方法。

解①递归记忆化

逐个读取价格表,分两个模式:等待购入和等待卖出。下面分别用 $\text{Buy}$ 和 $\text{Sell}$ 表示这两个模式,在 $\text{Buy}$ 模式下有两种选择:在当前价格日卖出和下一天再说,同样地 $\text{Sell}$ 模式也有两种选择,以当前价格卖出和下一天再说。

from enum import Enum, auto
from functools import cache
from typing import List

class Mode(Enum):
    Buy = auto(),
    Sell = auto()

def solve(prices: List[int]) -> int:
    n = len(prices)

    @cache
    def recur(mode: Mode, i: int) -> int:
        if i >= n:
            return 0

        if mode is Mode.Buy:
            cand1 = -prices[i] + recur(Mode.Sell, i+1)
            cand2 = recur(Mode.Buy, i+1)
        else:
            cand1 = prices[i] + recur(Mode.Buy, i+2)
            cand2 = recur(Mode.Sell, i+1)

        return max(cand1, cand2)

    return recur(Mode.Buy, 0)

运行时间:52 ms (beats 44.28%, ~100%),内存占用:21.06 MB (beats 31.05%)。

解②迭代DP

上面的递归实现,可以帮助我们理清普通 DP 版本的实现思路。

对于交易有限次数的股票模型,可以穷举所有所有轮次买和卖的状态,但是对于不限次数的模型,则不能这样穷举,只能区分买和卖,不能辨别轮次。

尝试从正面看这个问题,分别用 buy 和 sell 两个数组代表一个前缀价格串上的最后操作是买或卖带来的最大收益。

这样,

  1. 买的最大收益是要比较前一天买的最大收益(意味着当前没有买卖)以及包含冷冻期在内的卖的最大收益加上当前价格;

  2. 卖的最大收益则是前一天卖的最大收益(意味着当前没有买卖)或者前一天买的最大收益加上当前价格(当前卖出)

def solve(prices: List[int]) -> int:
    n = len(prices)

    if n == 1:
        return 0

    prices.append(0)
    buy = [0] * (n+1)
    buy[0] = -prices[0]
    buy[1] = max(buy[0], -prices[1])

    sell = [0] * n

    for i in range(n-1):
        buy[i + 2] = max(buy[i+1], sell[i] - prices[i + 2])
        sell[i + 1] = max(sell[i], buy[i] + prices[i + 1])

    return sell[n - 1]

运行时间:36 ms (beats 98.07%, ~100%),内存占用:16.39 MB (beats 99.34%) 。

当然,仔细观察代码,会发现 $O(n)$ 的内存占用是不必要的,实际上只需要维护三个持久变量:$\text{buy}_0$, $\text{buy}_1$ 和 $\text{sell}_0$ 。

def solve(prices: List[int]) -> int:
    n = len(prices)

    if n == 1:
        return 0

    prices.append(0)

    buy0 = -prices[0]
    buy1 = max(buy0, -prices[1])
    sell0 = 0

    for i in range(n-1):
        buy2 = max(buy1, sell0 - prices[i + 2])
        sell1 = max(sell0, buy0 + prices[i + 1])

        sell0 = sell1
        buy1, buy0 = buy2, buy1

    return sell0

运行时间:45 ms (beats 79.16%, ~100%),内存占用:16.56 MB (beats 87.33%. ~100%) 。

0714. Best Time to Buy and Sell Stock with Transaction Fee

Medium

不限次交易,有交易费

解①状态DP

直接按照前面交易冻结日的股票交易模型,易得

def solve(prices: List[int], fee: int) -> int:
    n = len(prices)
    buy = -prices[0]
    sell = 0

    for price in prices[1:]:
        buy1 = max(buy, sell-price)
        sell1 = max(sell, buy+price-fee)

        buy = buy1
        sell = sell1

    return sell

运行时间:566 ms (beats 77%, ~100%),内存占用:23.7 MB (beats 67.29%, ~100%) 。

解②贪心

但是还是有方法利用像 0122. Best Time to Buy and Sell Stock 问题那样直接地算法来解决。

只需要把交易费用加在购入价格上即可。

注意这里的 $\text{buy}$ 区别于分状态 DP 问题里面的 $\text{buy}$ ,它表示得不是购入的最大收益,而是最低价格。

def solve(prices: List[int], fee: int) -> int:
    buy = prices[0]+fee
    ans = 0

    for price in prices[1:]:
        if price + fee < buy:
            buy = price + fee
        elif price > buy:
            ans += price-buy
            buy = price

    return ans

运行时间:495 ms (beats 99.5%, ~100%),内存占用:23.7 MB (beats 67.29%, ~100%) 。

注解

  1. 虽然形式上压缩到了一天,但是我们的计算公式也暗示,实际上不可能同时更新,实质上全部更新还是至少需要四天,对于一次交易也至少需要两天 ↩

  2. 虽然在测试上也看不这个内存节省的区别来,但毕竟是 Python 嘛,内存模型并不节省 ↩

© minghu6

·

theme

Simplex theme logo

by golas