188. 买卖股票的最佳时机IV (Best Time to Buy and Sell Stock IV)

3,869 阅读4分钟

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

题目链接:188. 买卖股票的最佳时机4 。给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。

输入k=2,prices=[2,4,1]k = 2, prices = [2,4,1]

输出22

解释: 在第 11 天 (股票价格为 22) 的时候买入,在第 22 天 (股票价格为 44) 的时候卖出,这笔交易所能获得利润 42=24-2 = 2

输入k=2,prices=[3,2,6,5,0,3]k = 2, prices = [3,2,6,5,0,3]

输出77

解释: 在第 22 天 (股票价格为 22) 的时候买入,在第 33 天 (股票价格为 66) 的时候卖出, 这笔交易所能获得利润 = 62=46-2 = 4 。随后,在第 55 天 (股票价格 = 00) 的时候买入,在第 66 天 (股票价格 = 33) 的时候卖出, 这笔交易所能获得利润 = 30=33-0 = 3

在股票系列的前三期中,我们已经找到了买卖股票问题的最优子结构,即 dp[x][y][z]dp[x][y][z] 表示第 xx 天、股状态为 yy、交易次数为 zz 时的最大利润,其中

该最优子结构同样适用于本题,仅仅是 zz 的范围变成了 z[0,k]z \in [0, k]kk 为题设中给出的具体交易次数。

中规中矩的动态规划

1、确定 dp 状态数组与含义

定义 dp[x][y][z]dp[x][y][z] 为第 xx 天、股状态为 yy、交易次数为 zz 时的最大利润,其中,

  • 当前交易日 x[0,n)x \in [0, n)n=prices.lengthn = prices.length

  • 是否持股 y=0,1y = 0,1j=0j=0 指不持股,j=1j=1 指持股;

  • 交易次数 z[0,k]z \in[0,k],其中 kk 为最多交易的次数(题设给出)。

2、确定 dp 状态转移方程

  1. xx 天,未持股 (y=0)(y=0),交易 zz 次,其对应的状态为 dp[x][0][z]dp[x][0][z],从前一天的状态转移到当前的状态共有两种可能
  • x1x-1 天,未持股 (y=0)(y=0),交易已达 zz次,即 dp[x1][0][z]dp[x-1][0][z]

  • x1x-1 天,有持股 (y=1)(y=1),交易 z1z-1 次,在第 xx 天(当日)卖出,即 dp[x1][0][z1]+prices[x]dp[x-1][0][z-1] + prices[x]

    因此,当日的状态转移方程为 dp[x][0][z]=max(dp[x1][0][z],dp[x1][0][z1]+prices[x]dp[x][0][z] = max(dp[x-1][0][z], dp[x-1][0][z-1]+prices[x]

  1. xx 天,有持股 (y=1)(y=1),交易 zz 次,其对应的状态为 dp[x][1][z]dp[x][1][z],从前一天的状态转移到当前的状态共有两种可能
  • x1x-1 天,有持股 (y=1)(y=1),交易 zz 次,即 dp[x1][1][z]dp[x-1][1][z]

  • x1x-1 天,未持股 (y=0)(y=0),交易 zz 次,在第 xx 天(当日)买入,即 dp[x1][0][z]prices[x]dp[x-1][0][z]-prices[x]

    因此,当日的状态转移方程为 dp[x][1][z]=max(dp[x1][1][z],dp[x1][0][z1]prices[x]dp[x][1][z] = max(dp[x-1][1][z], dp[x-1][0][z-1]-prices[x]

3、确定 dp 初始状态

  1. 00 天,不持股状态 (y=0)(y=0)
  • dp[0][0][0]=0dp[0][0][0]=0,代表第 00 天,不持股,交易次数为 00 时,我们手上的现金

  • dp[0][0][z]=infinitydp[0][0][z]=-infinity [循环计算],代表第 00 天,不持股,交易次数为 zz 时,我们手上的利润(这是一个非法状态,设置成代码运行时环境内的最小安全整数即可)

  1. 00 天,持股状态 (y=1)(y=1)
  • dp[0][1][0]=prices[0]dp[0][1][0]=-prices[0],代表第 00 天,有持股,交易次数为 00 时,我们手上的最大利润(一买一卖才算一次交易,目前仅为买入,等这一股卖出时,交易次数再加 11

  • dp[0][1][z]=infinitydp[0][1][z]=-infinity [循环计算],代表第 00 天,有持股,交易次数为 zz 时,我们手上的最大利润(这是一个非法状态,设置成代码运行时环境内的最小安全整数即可)

  1. xx 天,交易 00 次的状态
  • dp[x][0][0]=0dp[x][0][0]=0,不管是第几天,没有持股也没有交易,那利润比为0

  • dp[x][1][0]=max(dp[x1][1][0],dp[x1][0][0]prices[x])dp[x][1][0]=max(dp[x - 1][1][0], dp[x - 1][0][0] - prices[x]),[循环计算],代表第 xx 天,有持股,交易次数为0时,我们手上的最大利润,这里直接套用状态转移方程即可。

4、确定遍历顺序

  1. 第一层遍历是 天数,从第 x=1x=1 天遍历到第 x=n1x=n-1

  2. 第二层遍历是 交易次数,但是这个有个小小 tricktrick,就是实际天数与交易次数的比较。有 nn 个交易日,最多能交易的次数为 n/2n/2,并向下取整,故实际的交易次数应为 min(n/2,k)min(n/2,k)。所以第二层遍历是从 z=1z=1z=min(n/2,k)z=min(n/2,k)

5、确定最后的返回值

找到第 n1n-1 天,不持有股票(j=0j=0),交易 zz 次中,元素最大的值,即 max(...dp[n1][0])max(...dp[n-1][0])

6、上代码

/**
 * 空间复杂度O(N * min(k, N/2))
 * 时间复杂度O(N * min(k, N/2))
 */
function maxProfit(k: number, prices: number[]): number {
    const len = prices.length;

    if (len < 2) return 0;

    const maxTradeTimes = Math.min(k, ~~(len / 2)) + 1; // 目的是要从交易0次开始

    const dp = Array.from(
        { length: len }, 
        () => [
            new Array(maxTradeTimes).fill(0),
            new Array(maxTradeTimes).fill(0),
        ],
    );

    dp[0][0][0] = 0;
    dp[0][1][0] = -prices[0];

    for(let x = 1; x < len; x++) {
        dp[x][1][0] = Math.max(dp[x - 1][1][0], dp[x - 1][0][0] - prices[x]);
    }
    for(let z = 1; z < maxTradeTimes; z++) {
        dp[0][0][z] = Number.MIN_SAFE_INTEGER;
        dp[0][1][z] = Number.MIN_SAFE_INTEGER;
    }

    for(let x = 1; x < len; x ++) {
        for(let z = 1; z < maxTradeTimes; z++) {
            dp[x][0][z] = Math.max(dp[x - 1][0][z], dp[x - 1][1][z - 1] + prices[x]);
            dp[x][1][z] = Math.max(dp[x - 1][1][z], dp[x - 1][0][z] - prices[x]);
        }
    }

    return Math.max(...dp[len - 1][0]);
}