【leetcode】123. 买卖股票的最佳时机3

题目内容

123. 买卖股票的最佳时机3

思路分析

本题使用了动态规划的方法,状态比较多,比较难想到。

方法一:动态规划

参考链接1,参考链接2

每天交易完成后有5种状态:

-1 不曾交易过

0 买过一次

1 买过一次,卖过一次

2 买过两次,卖过一次

3 买过两次,卖过两次

因为状态-1不会对现金产生影响,因此不记录此状态。设动态规划的目标为每天交易完成后持有的现金(假设一开始持有的现金是0,则最后持有的现金即为利润),根据这四种可写出状态。最后返回最后一天买过两次,卖过两次状态的现金,即为最大利润。

注意这里代码中参考了前几个股票题的思路,由于当天的状态仅由前一天的状态决定,因此只需要8个变量来记录状态(而不是大小为O(n)的数组),4个前一天+4个当天。

方法二:动态规划的小小优化

由于此题目的特殊性,可以非常巧妙地将方法一中的8个变量减少为4个变量,因此代码的内容也可以减少很多。详见代码中的注释及参考链接1。

ps. 提交后写的百分比还不错,虽然知道是假的,不过还是蛮爽的

执行用时:160 ms, 在所有 C++ 提交中击败了100.00%的用户

内存消耗:73.4 MB, 在所有 C++ 提交中击败了99.69%的用户

代码

#include <vector>

using namespace std;

class Solution {
public:
    // 动态规划
    // 时间复杂度O(n),空间复杂度O(1)
    int maxProfit1(vector<int>& prices) {
        int n = prices.size();

        if (n == 0 || n == 1) return 0;
        
        // 每天交易完成后有5种状态:
        // -1. 不曾交易过
        // 0. 买过一次
        // 1. 买过一次,卖过一次
        // 2. 买过两次,卖过一次
        // 3. 买过两次,卖过两次
        // 因为状态-1不会对现金产生影响,因此不记录此状态
        // cur_cash表示每天交易完成后的最大现金
        // vector<vector<int>> cur_cash(n, vector<int>(4));
        // 由于每天只用到了上一天的状态,因此可以使用8个变量来代替二维数组
        int pre_buy_one{ -prices[0] },  // 第一天买了股票则现金为-prices[0]
            pre_sell_one{ 0 },  // 第一天没股票可卖
            pre_buy_two{ -prices[0] },  // 第一天最多买一次股票
            pre_sell_two{ 0 },  // 第一天没股票可卖
            cur_buy_one{ 0 },
            cur_sell_one{ 0 },
            cur_buy_two{ 0 },
            cur_sell_two{ 0 };

        for (int i = 1; i < n; i++) {
            // 状态转移方程
            // 前一天买过一次/当天买了一次 -> 当天买过一次
            cur_buy_one = max(pre_buy_one, -prices[i]);  
            // 前一天卖过一次/前一天买过一次当天卖了一次 
            // -> 当天买过一次卖过一次
            cur_sell_one = max(pre_sell_one, cur_buy_one + prices[i]);
            // 前一天买过两次卖过一次/前一天买过一次卖过一次,当天又买了一次
            // -> 当天买过两次卖过一次
            cur_buy_two = max(pre_buy_two, pre_sell_one - prices[i]);
            // 前一天买过两次卖过两次/前一天买过两次卖过一次,当天又卖了一次 
            // -> 当天买过两次卖过两次
            cur_sell_two = max(pre_sell_two, pre_buy_two + prices[i]);

            pre_buy_one = cur_buy_one;
            pre_sell_one = cur_sell_one;
            pre_buy_two = cur_buy_two;
            pre_sell_two = cur_sell_two;
        }

        return cur_sell_two;  // 充分利用次数
    }

    // 动态规划优化
    // 上面状态转移方程中的pre_*可以换成cur_*,因此可以只使用4个变量而非8个变量。
    // 例如在计算cur_sell_one时,使用cur_sell_one而非pre_sell_one来进行状态转移
    // cur_buy_one比pre_buy_one多考虑了当天买了的情况,而状态转移至cur_sell_one
    // 时,考虑的是当天卖出的情况,同一天内的买卖并不会对现金产生影响。
    // 时间复杂度O(n),空间复杂度O(1)
    int maxProfit(vector<int>& prices) {
        int n = prices.size();

        if (n == 0 || n == 1) return 0;

        // 此时物理含义仍为每天交易完成后拥有的现金
        int buy_one{ -prices[0] },  // 第一天买了股票则现金为-prices[0]
            sell_one{ 0 },  // 第一天没股票可卖
            buy_two{ -prices[0] },  // 第一天最多买一次股票
            sell_two{ 0 };  // 第一天没股票可卖

        for (int i = 1; i < n; i++) {
            // 状态转移方程
            // 买过一次/买了一次 -> 买过一次
            buy_one = max(buy_one, -prices[i]);
            // 前一天卖过一次/前一天买过一次当天卖了一次 
            // -> 当天买过一次卖过一次
            sell_one = max(sell_one, buy_one + prices[i]);
            // 前买过两次卖过一次/买过一次卖过一次,又买了一次
            // -> 买过两次卖过一次
            buy_two = max(buy_two, sell_one - prices[i]);
            // 买过两次卖过两次/买过两次卖过一次,又卖了一次 
            // -> 买过两次卖过两次
            sell_two = max(sell_two, buy_two + prices[i]);
        }

        return sell_two;  // 充分利用次数
    }


    // 自己错误写法,想着挑最大的两段出来,结果发现此思想并不可取
    // 如[1,2,4,2,5,7,2,4,9,0]
    //          ↑   ↑ ↑   ↑
    //         买   卖买  卖
    // 按此思想会取7-2和9-2,但实际上最优解为7-1和9-2
    // [1,2,4,2,5,7,2,4,9,0]
    //  ↑         ↑ ↑   ↑
    //  买        卖买  卖
    // 感觉比较困难
    void update_top2_profit(int&max_profit, int& second_max_profit, int& cur_profit) {
        if (cur_profit >= max_profit) {
            second_max_profit = max_profit;
            max_profit = cur_profit;
        }
        else if (cur_profit >= second_max_profit) {
            second_max_profit = cur_profit;
        }
    }

    int maxProfit_error(vector<int>& prices) {
        int n = prices.size();

        if (n == 0 || n == 1) return 0;

        int max_profit{ 0 }, second_max_profit{ 0 },
            min_price{ prices[0] }, cur_profit{ 0 };

        for (int i = 1; i < n; i++) {
            if (i == n - 1 && prices[i] >= prices[i - 1]) {
                cur_profit = prices[i] - min_price;
                update_top2_profit(max_profit, second_max_profit, cur_profit);
            }
            else  if (prices[i] < prices[i - 1]) {
                cur_profit = prices[i - 1] - min_price;
                update_top2_profit(max_profit, second_max_profit, cur_profit);
                min_price = prices[i];
            }
        }

        return max_profit + second_max_profit;
    }
};


int main() {
    Solution s;
    vector<int> input{ 1,2,4,2,5,7,2,4,9,0 };

    int output;
    output = s.maxProfit(input);

    return output;
}

收获

动态规划算法及其优化。