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

3,839 阅读1分钟

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

题目链接:122. 买卖股票的最佳时机2 。给定一个数组 prices,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一只 股票。(买卖多次)返回你能获得的最大利润 。

输入:prices = [7,1,5,3,6,4]$

输出77

解释:在第 22 天(股票价格 = 11)的时候买入,在第 33 天(股票价格 = 55)的时候卖出, 这笔交易所能获得利润 = 51=45 - 1 = 4。   随后,在第 44 天(股票价格 = 33)的时候买入,在第 55 天(股票价格 = 66)的时候卖出, 这笔交易所能获得利润 = 63=36 - 3 = 3。 总利润为 4+3=74 + 3 = 7

输入prices=[1,2,3,4,5]prices = [1,2,3,4,5]

输出44

解释:在第 11 天(股票价格 = 11)的时候买入,在第 55 天 (股票价格 = 55)的时候卖出, 这笔交易所能获得利润 = 51=45 - 1 = 4 。   总利润为 44

121. 买卖股票的最佳时机 相比,去掉了 交易次数 的限制,因此我们去掉 dpdp 数组中 交易次数 这一维度。


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

1、确定 dp 状态数组以及含义

定义 dp[i][j]dp[i][j],在 [0,i][0,i] 天数区间内,持股状态为 jj 时的最大利润

其中,

  • ii 为天数,i[0,n)i \in [0, n)n=prices.lengthn=prices.length

  • jj 为是否持股,j=0j=01100 为未持股状态,11 为持股状态

2、确定 dp 状态转移方程

ii 天,未持股,即dp[i][0]dp[i][0],最大利润就有两种可能:

  • i1i-1 天,持股,第 ii 天卖出(交易变为1次),即 dp[i1][1]+prices[i]dp[i-1][1] + prices[i]

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

    我们取两者的最大值,即 dp[i][0]=max(dp[i1][1]+prices[i],dp[i1][0])dp[i][0] = max(dp[i-1][1] + prices[i], dp[i-1][0])

ii 天,持股,即 dp[i][1]dp[i][1],最大利润就有两种可能:

  • i1i-1 天,未持股,但第 ii 天买入,即 dp[i1][0]prices[i]dp[i-1][0] - prices[i]

  • i1i-1 天,持股,但第 ii 天什么也不干,即 dp[i1][1]dp[i-1][1]

    我们取两者的最大值,即 dp[i][1]=max(dp[i1][0]prices[i],dp[i1][1])dp[i][1] = max(dp[i-1][0] - prices[i], dp[i-1][1])

3、确定 dp 初始状态

  • dp[0][0]=0dp[0][0]=0 ,代表第 00 天不持股,手上最大的利润;

  • dp[0][1]=prices[0]dp[0][1]=-prices[0],代表第 00 天持股,手上最大的利润;

4、确定遍历顺序

从第 11 天遍历到第 prices.length1prices.length-1

5、确定返回值

n1n-1 天的最大利润,一定是手上不持股时的利润(即 j=0j=0),即返回 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 = new Array(n).fill(null).map(() => [0, 0]);

    dp[0][0] = 0;
    dp[0][1] = -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][0] - prices[i]);
    }

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

方法2: dp数组状态压缩

从法1的状态转移方程可以看出来,dp[i][0]dp[i][1]dp[i][0]、dp[i][1] 仅与 dp[i1][0]dp[i-1][0]dp[i1][1]dp[i-1][1] 有关系,故可将 dpdp 数组压缩成两个值 profitprofitcostedcosted

示例代码

/**
 * 空间复杂度:平均O(1)
 * 时间复杂度:平均O(n)
 */
function maxProfit(prices: number[]): number {
    const n = prices.length;
    if (n < 2) {
        return 0;
    }

    let profit = 0, costed = - prices[0];

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

    return profit;
}

🔴 NOTE: 上述示例代码中的 解构赋值 语法很重要。

[profit, costed] = [
    Math.max(profit, costed + prices[i]),
    Math.max(costed, profit - prices[i]),
];

等价于

const tempProfit = profit;
profit = Math.max(profit, costed + prices[i]);
costed = Math.max(costed, tempProfit - prices[i]);

拓展方法--贪心

由于股票的购买没有限制,因此整个问题等价于寻找 xx 个不相交区间 (li,ri](l_i, r_i] 使得,i=1xa[ri]a[li]\sum_{i=1}^x a[r_i] - a[l_i],其中 lil_i 表示第 lil_i 天买入,rir_i 表示第 rir_i 卖出。上式可整理为:

a[ri]a[li]=(a[ri]a[ri1])+(a[ri1]a[ri2])+...+(a[li+1]a[li])a[r_i] - a[l_i] = (a[r_i] - a[r_i - 1]) + (a[r_i - 1] - a[r_i - 2]) + ... + (a[l_i + 1] - a[l_i])

从贪心的角度考虑,我们每次选择贡献值大于 00 的区间即为最大收益,即 ans=i=1n1max(0,a[i]a[i1])ans = \sum_{i=1}^{n-1} max(0, a[i] - a[i-1])

示例代码

/**
 * 空间复杂度:平均O(1)
 * 时间复杂度:平均O(n)
 */
function maxProfit(prices: number[]): number {
    let profit = 0;

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

    return profit;
}