309. 最佳买卖股票时机含冷冻期(Best Time to Buy and Sell Stock with Cooldown)

3,908 阅读4分钟

"给血雨腥风的二级市场留下八个大字——巴菲特就那么回事"

题目链接:309. 最佳买卖股票时机含冷冻期,给定一个整数数组 prices,其中 prices[i] 表示第 i 天的股票价格 。设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票)。卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

输入: prices=[1,2,3,0,2]prices = [1,2,3,0,2]

输出: 33

解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]

该题目是 122. 买卖股票的最佳时机2 的延伸,交易次数不限,但是交易后会有一天的冷冻期(这一天只能静静地度过,不能再买入)。

仔细思考,当天是不是“冷冻期”和什么有关?答案:仅与前一天是否卖出有关。所以“冷冻期”是一个幌子,我们只需要区分不持股状态下是否有卖出行为即可,即如果有卖出行为,则次日不能买入。

方法1: 中规中矩的动态规划

1、确定 dp 数组以及含义

由于不能同时参与多笔交易,即必须卖出手中股票才能再次购买,或手中不持股时才能买入,因此每天交易结束后仅有两种情况,即,1、手中持股,2、手中不持股。

我们将手中不持股状态再细分,可以分为当前没卖出当天有卖出,故整体状态可分为三种,即, 1、手中持股,2、手中不持股,且没卖出(次日可以自由交易),3、手中不持股,且有卖出(次日只能乖乖等待)。

故,用动态规划数组 dp[i][j]dp[i][j] 表示第 ii 日持股状态为 jj 时,我们手上最大的利润(因为是“空手套白狼“的融资炒股,即手上没有本钱,只要交易结束后,手上的现金即为收益),其中,

  • i[0,n),n=prices.lengthi \in [0, n), n = prices.length

  • j=012j = 0、1、2 分别为,持股状态 (0)(0)、不持股且没卖出 (1)(1)、不持股且有卖出 (2)(2)

2、确定 dp 对应的状态方程

对于 dp[i][j]dp[i][j] 状态共有三种状态需要讨论并计算,即,

  • dp[i][0]dp[i][0]:第 ii 天在持股状态下的最大利润

  • dp[i][1]dp[i][1]:第 ii 天在不持股当天没卖出状态下的最大利润

  • dp[i][2]dp[i][2]:第 ii 天在我们不持股当天有卖出状态下的最大利润


dp[i][0]dp[i][0]:第 ii 天我们持有股票,存在两种可能,即

  • i1i - 1持股,即 dp[i1][0]dp[i-1][0]

  • i1i - 1不持股当天没卖出(因为第 i1i - 1 天一旦卖出了,第 ii 天就不能买入了),在第 ii 天买入了,即 dp[i1][1]prices[i]dp[i-1][1] - prices[i]

故,dp[i][0]=max(dp[i1][0],dp[i1][1]prices[i])dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i])


dp[i][1]dp[i][1]:第 ii 天我们没有股票,而且并不是因为卖了才没有的,所以从第 i1i-1 天向第 ii 天转移时,存在两种可能,即,

  • i1i - 1 天也不持股当天没卖出,即 dp[i1][1]dp[i-1][1]

  • i1i - 1 天也不持股当天有卖出,即 dp[i1][2]dp[i-1][2]

故,dp[i][1]=max(dp[i1][1],dp[i1][2])dp[i][1] = max(dp[i-1][1], dp[i-1][2])


dp[i][2]dp[i][2]:第 ii 天我们卖出了股票,说明前一天必须持有,故 dp[i][2]=dp[i1][0]+prices[i]dp[i][2] = dp[i-1][0] + prices[i]

3、确定 dp 初始状态

  • 00 天我们持股状态下的最大收益:dp[0][0]=prices[0]dp[0][0] = -prices[0]

  • 00 天我们不持股当前没卖出状态下的最大收益:dp[0][1]=0dp[0][1] = 0

  • 00 天我们不持股当前有卖出状态下的最大收益:dp[0][2]=0dp[0][2] = 0

4、确定遍历顺序

从第 00 天遍历到第 n1n-1

5、确定最终返回值

n1n - 1 天如还持有股票,即 dp[n1][0]dp[n-1][0],收益肯定不是最大的,所以仅需要比较 dp[n1]dp[n-1] 数组的第1、2项元素大小即可,故返回值为 max(dp[n1][1],dp[n1][2])max(dp[n-1][1], dp[n-1][2])

6、上代码

中规中矩

/**
 * 空间复杂度 O(n),n是prices数组的长度
 * 时间复杂度 O(n)
 */
function maxProfit(prices: number[]): number {
    const n = prices.length;
    const dp = Array.from({ length: n}, () => [0, 0, 0]);
    dp[0][0] = -prices[0];

    for(let i = 1; i < n; i++) {
        dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
        dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][2]);
        dp[i][2] = dp[i - 1][0] + prices[i];
    }

    return Math.max(dp[n - 1][1], dp[n - 1][2]);
};

状态压缩

/**
 * 空间复杂度 O(1)
 * 时间复杂度 O(n)
 */
function maxProfit(prices: number[]): number {
    let dp0 = -prices[0];
    let dp1 = 0;
    let dp2 = 0;

    for(let i = 1, len = prices.length; i < len; i++) {
        [dp0, dp1,dp2 ] = [
            Math.max(dp0, dp1 - prices[i]),
            Math.max(dp1, dp2),
            dp0 + prices[i],
        ]
    }

    return Math.max(dp1, dp2);
};

方法2: 思路进阶

冷冻期是一个干扰概念,实际上还是限制交易时机(即买入、卖出)。我们只需要关心第 ii 天手上有木有股票即可。

1、确定 dp 数组以及含义

用动态规划数组 dp[i][j]dp[i][j] 表示ii 日持股状态为 jj 时的最大收益,其中,

  • i[0,n),n=prices.lengthi \in [0, n), n = prices.length

  • j=01j = 0、1,分别为,没持股 (0)(0)、有持股 (1)(1)

2、确定 dp 对应的状态方程

对于 dp[i][j]dp[i][j] 状态共有两种状态需要讨论并计算,即,

  • dp[i][0]dp[i][0]:第 ii 天在我们没持股状态下的最大收益

  • dp[i][1]dp[i][1]:第 ii 天在我们有持股状态下的最大收益


dp[i][0]dp[i][0]:第 ii 天我们没有持有股票,存在两种可能,即

  • i1i-1 天也没持股,即 dp[i1][0]dp[i-1][0]

  • i1i - 1有持股且在次日卖出,即 dp[i1][1]+prices[i]dp[i-1][1] + prices[i]

故,dp[i][0]=max(dp[i1][0],dp[i1][1]+prices[i])dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])


dp[i][1]dp[i][1]:第 ii 天我们持有股票,存在两种可能,即,

  • i1i - 1 天也有持股,即 dp[i1][0]dp[i-1][0]

  • i1i - 1 天是冷静期,第 i2i - 2 天没持股的状态下,当日买入,即 dp[i2][0]prices[i]dp[i-2][0] - prices[i]💥

故,dp[i][1]=max(dp[i1][1],dp[i2][0]prices[i])dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i])

3、确定 dp 初始状态

NOTE: 状态转移方程中涉及到了 i2i - 2,故在初始化时要考虑交易前两天的情况,即 i=0i=0i=1i=1

  • 00 天我们不持股状态下的最大收益,dp[0][0]=0dp[0][0] = 0

  • 00 天我们持股状态下的最大收益,dp[0][1]=prices[0]dp[0][1] = -prices[0]

  • 11 天我们不持股状态下的最大收益(只是第 11 天的股价大于第 00 天的股价,才有正收益,否则为 00),dp[1][0]=max(prices[1]prices[0],0)dp[1][0] = max(prices[1]-prices[0], 0)

  • 11 天我们持股状态下的最大收益(第 00 天与第 11 天只有一次买入机会,选择价格较小的那一天买入即可),dp[1][1]=max(prices[0],prices[1])dp[1][1] = max(-prices[0],-prices[1])

4、确定遍历顺序

从第 00 天遍历到第 n1n-1

5、确定最终返回值

n1n-1 天还持股肯定不是最大的利润,故返回 dp[n1][0]dp[n-1][0]

6、上代码

/**
 * 空间复杂度 O(n),n是prices数组的长度
 * 时间复杂度 O(n)
 */
function maxProfit(prices: number[]): number {
    const n = prices.length;

    if (n < 2) return 0;

    const dp = Array.from({ length: n }, () => [0, 0]);
    dp[0][0] = 0;
    dp[0][1] = -prices[0];
    dp[1][0] = Math.max(prices[1] - prices[0], 0);
    dp[1][1] = Math.max(-prices[0], -prices[1]);

    for(let i = 2; i < n; i++) {
        dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
        dp[i][1] = Math.max(dp[i - 1][1], dp[i - 2][0] - prices[i]);
    }

    return dp[n - 1][0];
};

参考

重识动态规划