Leetcode之买卖股票题目总结

355 阅读7分钟

关于股票买卖的题目,都可以用动态规划来解决,我们要根据具体题目,分析不同的初始条件以及转移方程。在接下来的每一道题目中,我都会提供使用动态规划方法的解答,以及一些针对某道特定的题目,更为简单的解答思路。

买卖股票的最佳时机Ⅰ

题目描述

动态规划

假设以dp[i]来表示第i天的最大利润,我们以dp[i][0]和dp[i][1]来分别代表今天结束时手上持有股票的状态和不持有状态的股票。
对于今天结束时持有股票的状态,有可能是昨天结束时已经持有了股票,今天没有进行任何操作,也有可能是今天以prices[i]的价格买入了股票。因此转移方程为dp[i][0] = Math.max{dp[i-1][0], -prices[i]}。 而如果今天结束时不持有股票,那么可能是昨天结束时已经不持有股票了,今天没有进行任何操作,也可能是昨天结束时持有股票,今天以prices[i]的价格卖出。那么转移方程为dp[i][1] = Math.max{dp[i-1][1], dp[i-1][0] + prices[i]}
初始条件:第一天结束若是持有股票,则利润一定是-prices[i],即dp[0][0] = -prices[i]。第一天结束若不持有股票,一定是没有买也没有卖,即dp[i][1] = 0
我们可以观察到,第i天的状态只与第i-1天的状态有关,因此我们想到了空间优化的措施,用两个变量存储前一天两种状态下的最大利润,避免创建二维数组:

var maxProfit = function(prices) {
    let n = prices.length;
    let buy = -prices[0];
    let sell = 0;
    for(let i=1;i<n;i++){
 		let s0 = buy, s1 = sell;
        buy = Math.max(s0, -prices[i])
        sell = Math.max(s1, s0 + prices[i])
    }
    return sell;
};

计算最大的差值

对这道题来说,动态规划其实有点小题大做了。由于只能进行一次交易,我们只需要知道prices[j] - prices[i] (j>i) 的最大值。用一个minPrice来维护最小买入价格,以profit来记录当前的最大利润,遍历一遍数组,比较得出当前的最大利润以及更新minPrice。

var maxProfit = function(prices) {
    let minPrice = prices[0];
    let profit = 0;
    for(let i=1; i<prices.length;i++){
        if(prices[i] < minPrice){
            minPrice = prices[i]
        }else{
            profit = Math.max(profit, prices[i] - minPrice)
        }
    }
    return profit;
};

买卖股票的最佳时机Ⅱ

题目描述

动态规划

这道题和第一题的唯一区别是可以进行无限次的交易。如果今天结束的状态是持有股票,那么前一天有可能是完成了一次交易,卖掉了股票。因此dp[i][0]的转移方程就变成了dp[i][0] = Math.max{dp[i-1][0], dp[i-1][1] - prices[i]}(因为初始条件dp[i][1] = 0,所以如果是进行第一次交易,也不会影响结果)。 和上一题一样,我们也可以使用两个变量进行空间的优化。

var maxProfit = function(prices) {
    let n = prices.length;
    if(n == 0) return 0;
    let buy = -prices[0];
    let sell = 0;
    for(let i = 1; i < n; i++){
        let a = buy, b = sell;
        buy = Math.max(a, b - prices[i]);
        sell = Math.max(b, a + prices[i]);
    }
    return sell;
};

贪心

由于交易次数不限,我们可以这样想:加入今天的价格比明天低,那么我们就在今天买入,明天卖出,这也必能增加利润。如果价格连续几天都攀升,我们使用这种方法就相当于在价格最低点买入,最高点卖出。这样我们遍历一次数组就能得到最大利润。

var maxProfit = function(prices) {
    let profit = 0;
    if(prices.length == 0) return 0;
    let i = 1;
    while(i < prices.length){
        if(prices[i] > prices[i-1]){
            profit += prices[i] - prices[i-1]
        }
        i++;
    }
    return profit;
};

买卖股票的最佳时机Ⅲ

题目描述

动态规划1.0

在前面题目的基础上,这道题目加了一个限制,最多完成两笔交易,那么一天结束之后,我们可能有五种状态:

  1. 没有过进行任何交易
  2. 持有第一支股票(第一笔交易中)
  3. 完成了第一次交易(买和卖),现在不持有股票
  4. 持有第二支股票(第二笔交易中)
  5. 完成第二次交易(买和卖),现在不持有股票 第一个状态,也是初始状态,利润一定为0,因此不必维护与更新。 我们用dp[i][0]、dp[i][1]、dp[i][2]、dp[i][3]分别表示第i天2-5状态下的最大利润。用-Infinity来表示不可能的状态(如果第一笔交易没完成或收益不为正,就不进行第二次交易)。那么
dp[0][0] = -prices[0]
dp[0][1] = 0
dp[0][2] = -Infinity
dp[0][3] = -Infinity

接下来考虑每一天的转移方程:

// 第一笔交易的转移方程不必多说
dp[i][0] = Math.max(dp[i-1][0], -prices[i])
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + prices[i]);
// 如果前一天第一笔交易结束时收益不为正,说明第一笔交易没有意义也无效,不应该有第二次交易,
// 因此把最大利润设为-Infinity
// 如果前一天第一笔交易结束时收益大于0,今天结束的状态是持有第二支股票
// 那么有可能是延续了前一天的状态或是昨天卖出第一支股票,今天买入第二支股票
dp[i][2] = dp[i-1][1] > 0 ? Math.max(dp[i-1][1] - prices[i], dp[i-1][2]) : -Infinity
// 如果前一天不可能买入第二天股票,那么今天也不可能售出
// 如果dp[i-1][2]有效,那么有可能延续昨天的状态或是在昨天买入的基础上卖出股票
dp[i][3] = dp[i-1][2] != -Infinity ? Math.max(dp[i-1][3], dp[i-1][2] + prices[i]) : -Infinity

最后返回最大利润,有可能没有进行交易、进行一次交易、或进行了两次交易。因此返回这三个的最大值就可以了,也就是Math.max(0, dp[prices.length-1][1], dp[prices.length-1][3])

var maxProfit = function(prices) {
    let n = prices.length;
    if(n == 0) return 0;
    let dp = [];
    dp[0] = [];
    dp[0][0] = -prices[0]; 
    dp[0][1] = 0;
    dp[0][2] = -Infinity; dp[0][3] = -Infinity;
    for(let i = 1; i < n; i++){
        dp[i] = [];
        dp[i][0] = Math.max(dp[i-1][0], -prices[i])
        dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + prices[i]);
        dp[i][2] = dp[i-1][1] > 0 ? Math.max(dp[i-1][1] - prices[i], dp[i-1][2]) : -Infinity
        dp[i][3] = dp[i-1][2] != -Infinity ? Math.max(dp[i-1][3], dp[i-1][2] + prices[i]) : -Infinity
    }
    return Math.max(0, dp[n-1][3], dp[n-1][1])
};

动态规划2.0

无论题目是否允许同一天买入并卖出,最终答案都不会受到影响,因为这一操作的收益为0。 在这一思想基础上,我们考虑对动态规划作优化。 我们用buy1、sell1、buy2、sell2分别代表2-4状态的最大利润。
边界条件(第一天结束时):

buy1 = -prices[0] 
sell1 = 0 
buy2 = -prices[0] 
sell2 = 0

接下来考虑转移方程,

buy1 = Math.max{buy1, -prices[i]}
sell1 = Math.max{sell1, buy1 + prices[i]}
buy2 = Math.max{buy2, sell1 - prices[i]}
sell2 = Math.max{sell2, buy2 + prices[i]}

有了前面题目的铺垫,得到这个转移方程并不难,只是这次我们没有用临时变量存储前一天的最大利润,而是直接计算。这样做会对结果有影响吗?答案是不会。比如在计算第i天的sell1的时候,我们用到的变量是第i天的buy1,它多考虑了第i天买入股票的情况,而这对计算sell1(第i天卖出股票)不会有任何影响,因为第i天买入又在第i天卖出,利润为0,对答案不会有影响。同理,计算buy2和sell2时,也可以直接用当天的值来算。
最后返回最大利润,必然是sell1、sell2和0之中的最大值,由于sell1和sell2的初始状态就是0,所以不用额外和0作比较。如果最后最好的情况是只进行一次交易,由于我们允许了同一天买和卖,sell1和sell2其实是相等的,因此最终返回的最大利润其实就是sell2。
这样,我们就得到了解答:

var maxProfit = function(prices) {
    let n = prices.length;
    if(n == 0) return 0;
    let buy1 = -prices[0], sell1 = 0;
    let buy2 = -prices[0], sell2 = 0;
    for(let i = 1; i < n; i++){
        buy1 = Math.max(buy1, -prices[i]);
        sell1 = Math.max(sell1, buy1 + prices[i]);
        buy2 = Math.max(buy2, sell1 - prices[i]);
        sell2 = Math.max(sell2, buy2 + prices[i]);
    }
    return sell2;
};

买卖股票的最佳时机Ⅳ

题目描述 上一题最多允许2次交易,我们用4的变量来记录不同状态的最大利润。这道题目最大能进行k次交易,那么就一共会有2k个状态,因此,我们可以考虑用两个数量为k的数组表示第k次交易买和卖情况下的最大利润。初始状态和转移方程和上一题差不多。直接上代码:

var maxProfit = function(k, prices) {
    let n = prices.length;
    if(n == 0) return 0;
    //分别用一个数组记录第k次买入或卖出状态的最大利润
    //初始状态:buy[1] = ... = buy[k] = -prices[0] sell[1] = ... = sell[k] = 0
    //假设允许当天即买即卖,那样子利润为0,对结果不会有影响
    let buy = [], sell = [];
    for(let i = 0; i <= k; i++){
        buy[i] = -prices[0];
        sell[i] = 0;
    }
    for(let d = 1; d < n; d++){
        for(let i = 1; i <= k; i++){
            buy[i] = Math.max(buy[i], sell[i-1] - prices[d])
            sell[i] = Math.max(sell[i], buy[i] + prices[d])
        }
    }
    return sell[k];
};

到这,我们就由易而难的解决了买卖股票的最佳时机四道题。他们都可以用动态规划解决,只是我们要根据具体的题目条件确定边界值和转移方程。