美文网首页
算法 | 《买卖股票的最佳时机》系列问题

算法 | 《买卖股票的最佳时机》系列问题

作者: 格致匠心 | 来源:发表于2020-02-13 21:48 被阅读0次

    买卖股票的最佳时机系列题目是leetcode的经典系列题目。对于这系列题目可以用一种思想解决之,也就是DP状态转移思想。
    本笔记收录了Leetcode上的一个题解:一个方法团灭 6 道股票问题.
    但其实通解有时不是很直观,所以同时也记录对于这系列问题的特解和有趣的思想。

    一、通用解法思想

    (1)状态:

    这个系列问题的「状态」有三个,第一个是天数,第二个是允许交易的最大次数,第三个是当前的持有状态(即之前说的 rest 的状态,我们不妨用 1 表示持有,0 表示没有持有)。然后我们用一个三维数组就可以装下这几种状态的全部组合:

    dp[i][k][0 or 1]
    0 <= i <= n-1, 1 <= k <= K
    n 为天数,大 K 为最多交易数
    此问题共 n × K × 2 种状态,全部穷举就能搞定。
    for 0 <= i < n:
        for 1 <= k <= K:
            for s in {0, 1}:
                dp[i][k][s] = max(buy, sell, rest)
    
    (2)状态转移:
    状态转移
    dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
                  max(   选择 rest  ,           选择 sell      )
    
    解释:今天我没有持有股票,有两种可能:
    要么是我昨天就没有持有,然后今天选择 rest,所以我今天还是没有持有;
    要么是我昨天持有股票,但是今天我 sell 了,所以我今天没有持有股票了。
    
    dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
                  max(   选择 rest  ,           选择 buy         )
    
    解释:今天我持有着股票,有两种可能:
    要么我昨天就持有着股票,然后今天选择 rest,所以我今天还持有着股票;
    要么我昨天本没有持有,但今天我选择 buy,所以今天我就持有股票了。
    
    (3)基态:

    有了状态转移方程后,我们还需要一个基态,从这个基态依靠这些方程推出每个状态的结果。

    base case:
    dp[-1][k][0] = dp[i][0][0] = 0
    dp[-1][k][1] = dp[i][0][1] = -infinity
    
    状态转移方程:
    dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
    dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
    

    二、题目解法:

    1. 买卖股票的最佳时机

    K=1 通用解法:

    dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
    dp[i][1][1] = max(dp[i-1][1][1], dp[i-1][0][0] - prices[i]) 
                = max(dp[i-1][1][1], -prices[i])
    解释:k = 0 的 base case,所以 dp[i-1][0][0] = 0。
    
    现在发现 k 都是 1,不会改变,即 k 对状态转移已经没有影响了。
    可以进行进一步化简去掉所有 k:
    dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
    dp[i][1] = max(dp[i-1][1], -prices[i])
    
    for (int i = 0; i < n; i++) {
        if (i - 1 == -1) {
            dp[i][0] = 0;
            // 解释:
            //   dp[i][0] 
            // = max(dp[-1][0], dp[-1][1] + prices[i])
            // = max(0, -infinity + prices[i]) = 0
            dp[i][1] = -prices[i];
            //解释:
            //   dp[i][1] 
            // = max(dp[-1][1], dp[-1][0] - prices[i])
            // = max(-infinity, 0 - prices[i]) 
            // = -prices[i]
            continue;
        }
        dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
        dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
    }
    return dp[n - 1][0];
    

    通解简化:

    // k == 1
    int maxProfit_k_1(int[] prices) {
        int n = prices.length;
        // base case: dp[-1][0] = 0, dp[-1][1] = -infinity
        int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
        for (int i = 0; i < n; i++) {
            // dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
            dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
            // dp[i][1] = max(dp[i-1][1], -prices[i])
            dp_i_1 = Math.max(dp_i_1, -prices[i]);
        }
        return dp_i_0;
    }
    

    特解:

    var maxProfit = function(prices) {
        // 维护一个最小值
        // 维护一个maxProfit
        let min = prices[0], maxProfit = 0
        for(const price of prices) {
            if(price < min) {
                min = price
            } else {
                if(price - min > maxProfit) {
                    maxProfit = price - min
                }
            }
        }
        return maxProfit
    };
    

    2. 买卖股票的最佳时机 II

    K=+Inf 通用解法:

    dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
    dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
                = max(dp[i-1][k][1], dp[i-1][k][0] - prices[i])
    
    我们发现数组中的 k 已经不会改变了(k是无穷大 k-1近似于k),也就是说不需要记录 k 这个状态了:
    dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
    dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
    
    int maxProfit_k_inf(int[] prices) {
        int n = prices.length;
        int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
        for (int i = 0; i < n; i++) {
            int temp = dp_i_0; // 因为dp_i_1需要的是上个状态的dp_i_0
            dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
            dp_i_1 = Math.max(dp_i_1, temp - prices[i]);
        }
        return dp_i_0;
    }
    

    特解:每次只要是上升都买和卖,这样就可以无限获利

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

    3. 最佳买卖股票时机含冷冻期

    K=+Inf with cooldown 通解:
    每次 sell 之后要等一天才能继续交易。只要把这个特点融入上一题的状态转移方程即可:

    dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
    dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i])
    解释:第 i 天选择 buy 的时候,要从 i-2 的状态转移,而不是 i-1 。
    
    int maxProfit_with_cool(int[] prices) {
        int n = prices.length;
        int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
        int dp_pre_0 = 0; // 代表 dp[i-2][0]
        for (int i = 0; i < n; i++) {
            int temp = dp_i_0;
            dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
            dp_i_1 = Math.max(dp_i_1, dp_pre_0 - prices[i]);
            dp_pre_0 = temp;
        }
        return dp_i_0;
    }
    

    4. 买卖股票的最佳时机含手续费

    K=+Inf with fee:
    每次交易要支付手续费,只要把手续费从利润中减去即可。改写方程:

    dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
    dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee)
    解释:相当于买入股票的价格升高了。
    在第一个式子里减也是一样的,相当于卖出股票的价格减小了。
    
    int maxProfit_with_fee(int[] prices, int fee) {
        int n = prices.length;
        int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
        for (int i = 0; i < n; i++) {
            int temp = dp_i_0;
            dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
            dp_i_1 = Math.max(dp_i_1, temp - prices[i] - fee);
        }
        return dp_i_0;
    }
    

    5. 买卖股票的最佳时机 III

    K=2:
    这道题 k = 2 和后面要讲的 k 是任意正整数的情况中,对 k 的处理就凸显出来了。我们直接写代码,边写边分析原因。

    原始的动态转移方程,没有可化简的地方
    dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
    dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
    
    int max_k = 2;
    int[][][] dp = new int[n][max_k + 1][2];
    for (int i = 0; i < n; i++) {
        for (int k = max_k; k >= 1; k--) {
            if (i - 1 == -1) { /*处理 base case */ }
            dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
            dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
        }
    }
    // 穷举了 n × max_k × 2 个状态,正确。
    return dp[n - 1][max_k][0];。
    

    这里 k 取值范围比较小,所以可以不用 for 循环,直接把 k = 1 和 2 的情况手动列举出来也可以:

    dp[i][2][0] = max(dp[i-1][2][0], dp[i-1][2][1] + prices[i])
    dp[i][2][1] = max(dp[i-1][2][1], dp[i-1][1][0] - prices[i])
    dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
    dp[i][1][1] = max(dp[i-1][1][1], -prices[i])
    
    int maxProfit_k_2(int[] prices) {
        int dp_i10 = 0, dp_i11 = Integer.MIN_VALUE;
        int dp_i20 = 0, dp_i21 = Integer.MIN_VALUE;
        for (int price : prices) {
            dp_i20 = Math.max(dp_i20, dp_i21 + price);
            dp_i21 = Math.max(dp_i21, dp_i10 - price);
            dp_i10 = Math.max(dp_i10, dp_i11 + price);
            dp_i11 = Math.max(dp_i11, -price);
        }
        return dp_i20;
    }
    
    

    6. 买卖股票的最佳时机 IV

    K = Any integer
    有了上一题 k = 2 的铺垫,这题应该和上一题的第一个解法没啥区别。但是出现了一个超内存的错误,原来是传入的 k 值会非常大,dp 数组太大了。现在想想,交易次数 k 最多有多大呢?
    一次交易由买入和卖出构成,至少需要两天。所以说有效的限制 k 应该不超过 n/2,如果超过,就没有约束作用了,相当于 k = +infinity。这种情况是之前解决过的。
    直接把之前的代码重用:

    int maxProfit_k_any(int max_k, int[] prices) {
        int n = prices.length;
        if (max_k > n / 2) 
            return maxProfit_k_inf(prices);
    
        int[][][] dp = new int[n][max_k + 1][2];
        for (int i = 0; i < n; i++) 
            for (int k = max_k; k >= 1; k--) {
                if (i - 1 == -1) { /* 处理 base case */ }
                dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
                dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);     
            }
        return dp[n - 1][max_k][0];
    }
    

    相关文章

      网友评论

          本文标题:算法 | 《买卖股票的最佳时机》系列问题

          本文链接:https://www.haomeiwen.com/subject/dsgkfhtx.html