1. 买卖股票的最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

int maxProfit(vector<int>& prices) {
    int min_price = INT_MAX, max_profit = 0;
    for(auto price : prices) {
        max_profit = max(max_profit, price - min_price);
        min_price = min(min_price, price);
    }
    return max_profit;
}

时间复杂度:O(n),只需要遍历一次。
空间复杂度:O(1),只使用了常数个变量。

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

给定一个数组 prices ,其中 prices[i] 表示股票第 i 天的价格。在每一天,你可能会决定购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以购买它,然后在同一天出售。返回你能获得的最大利润 。

解法一:动态规划

考虑到「不能同时参与多笔交易」,因此每天交易结束后只可能存在手里有一支股票或者没有股票的状态。

定义状态dp[i][0]表示第 i 天交易完后手里没有股票的最大利润,dp[i][1]表示第i天交易完后手里持有一支股票的最大利润(i从0开始)。

考虑dp[i][0]的转移方程,如果这一天交易完后手里没有股票,那么可能的转移状态为前一天已经没有股票,即dp[i-1][0],或者前一天结束的时候手里持有一支股票,即dp[i-1][1],这时候我们要将其卖出,并获得price[i]的收益。因此为了收益最大化,我们列出如下的转移方程:

dp[i][0] = max{dp[i-1][0], dp[i-1][1]+prices[i]}

再来考虑dp[i][1],按照同样的方式考虑转移状态,那么可能的转移状态为前一天已经持有一支股票,即dp[i−1][1],或者前一天结束时还没有股票,即dp[i−1][0],这时候我们要将其买入,并减少prices[i]的收益。可以列出如下的转移方程:

dp[i][1] = max{dp[i-1][1], dp[i-1][0]-prices[i]}

对于初始状态,根据状态定义我们可以知道第 00 天交易结束的时候 dp[0][0]=0dp[0][1]=−prices[0]

因此,我们只要从前往后依次计算状态即可。由于全部交易结束后,持有股票的收益一定低于不持有股票的收益,因此这时候dp[n−1][0]的收益必然是大于 dp[n−1][1]的,最后的答案即为dp[n−1][0]

int maxProfit(vector<int>& prices) {
    int n = prices.size();
    int dp[n][2];
    dp[0][0] = 0, dp[0][1] = -prices[0];
    for (int i = 1; i < n; ++i) {
        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]);
    }
    return dp[n - 1][0];
}

时间复杂度:O(n)
空间复杂度:O(n)。我们需要开辟 O(n)空间存储动态规划中的所有状态。如果使用空间优化,空间复杂度可以优化至O(1)。优化后如下:

int maxProfit(vector<int>& prices) {
    int n = prices.size();
    int dp0 = 0, dp1 = -prices[0];
    for (int i = 1; i < n; ++i) {
        int newDp0 = max(dp0, dp1 + prices[i]);
        int newDp1 = max(dp1, dp0 - prices[i]);
        dp0 = newDp0;
        dp1 = newDp1;
    }
    return dp0;
}

解法二:贪心

由于股票的购买没有限制,贪心的角度考虑我们每次选择贡献大于0的区间即能使得答案最大化

int maxProfit(vector<int>& prices) {   
    int ans = 0;
    int n = prices.size();
    for (int i = 1; i < n; ++i) {
        ans += max(0, prices[i] - prices[i - 1]);
    }
    return ans;
}

时间复杂度:O(n)
空间复杂度:O(1)

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

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

解法:动态规划

由于我们最多可以完成两笔交易,因此在任意一天结束之后,我们会处于以下五个状态中的一种:
1)未进行过任何操作;
2)只进行过一次买操作;
3)进行了一次买操作和一次卖操作,即完成了一笔交易;
4)在完成了一笔交易的前提下,进行了第二次买操作;
5)完成了全部两笔交易。

由于第一个状态的利润显然为 00,因此我们可以不用将其记录,对于剩下的四个状态,我们分别将它们的最大利润记为 buy1, sell1, buy2, sell2.

对于 buy1 而言,在第 i 天我们可以不进行任何操作,保持不变,也可以在未进行任何操作的前提下以 prices[i] 的价格买入股票,那么的状态转移方程即为:

        buy1 = max(buy1', -prices[i])

对于 sell1 而言,在第 i 天我们可以不进行任何操作,保持不变,也可以在只进行过一次买操作的前提下以 prices[i] 的价格卖出股票,那么 sell1 的状态转移方程即为:

        sell1 = max(sell1', buy1 + prices[i])

同理我们可以得到 buy2, sell2 对应的状态转移方程:

        buy2 = max(buy2', sell1 - prices[i])
        sell2 = max(sell2', buy2 + prices[i])

那么对于边界条件,我们考虑第 i=0 天时的四个状态:buy1 即为以 prices[0] 的价格买入股票,因此buy1=−prices[0]
sell1即为在同一天买入并且卖出,因此sell1=0buy2即为在同一天买入并且卖出后再以 prices[0] 的价格买入股票,因此 buy2=−prices[0];同理可得 sell2=0。我们将这四个状态作为边界条件,从 i=1 开始进行动态规划,即可得到答案。

在动态规划结束后,由于我们可以进行不超过两笔交易,因此最终的答案在0, sell1, sell2中,且为三者中的最大值。然而我们可以发现,由于在边界条件中 sell1sell2的值已经为 0,并且在状态转移的过程中我们维护的是最大值,因此 sell1sell2最终一定大于等于 0。同时,如果最优的情况对应的是恰好一笔交易,那么它也会因为我们在转移时允许在同一天买入并且卖出这一宽松的条件,从 sell1转移至 sell2,因此最终的答案即为 sell2

int maxProfit(vector<int>& prices) {
    int n = prices.size();
    int buy1 = -prices[0], sell1 = 0;
    int buy2 = -prices[0], sell2 = 0;
    for (int i = 1; i < n; ++i) {
        buy1 = max(buy1, -prices[i]);
        sell1 = max(sell1, buy1 + prices[i]);
        buy2 = max(buy2, sell1 - prices[i]);
        sell2 = max(sell2, buy2 + prices[i]);
    }
    return sell2;
}

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

给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

解法:动态规划(通用模版)

dp[j][k] 为 j 次、k 持股状态(0 不持股,1 持股)的金钱

则对于 dp[j][0],即 j 次不持股,显然来源有 j 次不持股 dp[j][0],和 j-1 次持股 dp[j-1][1],故我们取来源的较大值即可。

dp[j][0] = max(dp[j-1][0], dp[j-1] + prices[i])

对于 dp[j][1],即 j 次持股,来源有 j 次持股、j 次不持股,故

dp[j][1] = max(dp[j][1], dp[j-1][0] - prices[i])

而每个状态都是一天,故我们再引入一个维度 i 表示天,即 dp[i][j][k],而每天都是由前一天转移而来的。故显然

dp[i-1][j][0] = max(dp[i-1][j-1][0], dp[i-1][j-1][1] + prices[i])

dp[i-1][j][1] = max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i])

int maxProfit(int k, vector<int>& prices) {
    if (prices.empty()){
        return 0;            
    }

    //i天,j次数,k持股状态
    vector<vector<vector<int>>> dp(prices.size(),vector<vector<int>>(k+1,vector<int>(2)));

    for (int i=0;i<prices.size();i++){
        for (int j=1;j<k+1;j++){
            if (i==0){
                dp[i][j][1]=-prices[i];
                continue;
            }
            dp[i][j][0]=max(dp[i-1][j][0],dp[i-1][j][1]+prices[i]);
            dp[i][j][1]=max(dp[i-1][j][1],dp[i-1][j-1][0]-prices[i]);
        }
    }

    return dp[prices.size()-1][k][0];
}

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

给定一个整数数组prices,其中第  prices[i] 表示第 i 天的股票价格 。设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

解法:动态规划

我们用 f[i] 表示第 i 天结束之后的「累计最大收益」。根据题目描述,由于我们最多只能同时买入(持有)一支股票,并且卖出股票后有冷冻期的限制,因此我们会有三种不同的状态:

1)目前持有一支股票,对应的「累计最大收益」记为 f[i][0]
2)目前不持有任何股票,并且处于冷冻期中,对应的「累计最大收益」记为 f[i][1]
3)目前不持有任何股票,并且不处于冷冻期中,对应的「累计最大收益」记为 f[i][2]

如何进行状态转移呢?在第 i 天时,我们可以在不违反规则的前提下进行「买入」或者「卖出」操作,此时第 ii 天的状态会从第 i-1 天的状态转移而来;我们也可以不进行任何操作,此时第 i 天的状态就等同于第 i-1 天的状态。那么我们分别对这三种状态进行分析:

对于 f[i][0],我们目前持有的这一支股票可以是在第 i−1 天就已经持有的,对应的状态为 f[i-1][0];或者是第 i 天买入的,那么第 i−1 天就不能持有股票并且不处于冷冻期中,对应的状态为 f[i−1][2] 加上买入股票的负收益 prices[i]。因此状态转移方程为:

    f[i][0] = max(f[i - 1][0], f[i - 1][2] - prices[i])

对于 f[i][1],我们在第 i 天结束之后处于冷冻期的原因是在当天卖出了股票,那么说明在第 i−1 天时我们必须持有一支股票,对应的状态为 f[i-1][0] 加上卖出股票的正收益 prices[i]。因此状态转移方程为:

    f[i][1] = f[i - 1][0] + prices[i]

对于 f[i][2],我们在第 i 天结束之后不持有任何股票并且不处于冷冻期,说明当天没有进行任何操作,即第 i−1 天时不持有任何股票:如果处于冷冻期,对应的状态为 f[i−1][1];如果不处于冷冻期,对应的状态为 f[i−1][2]。因此状态转移方程为:

    f[i][2] = max(f[i - 1][1], f[i - 1][2])

注意到如果在最后一天(第 n−1 天)结束之后,手上仍然持有股票,那么显然是没有任何意义的。因此更加精确地,最终的答案实际上是 f[n-1][1]f[n-1][2] 中的较大值

    max(f[n - 1][1], f[n - 1][2])

边界条件: 在第 0 天时,如果持有股票,那么只能是在第 0 天买入的,对应负收益 −prices[0];如果不持有股票,那么收益为零。

    f[0][0] = -prices[0]
    f[0][1] = 0
    f[0][2] = 0

这样我们就可以从第 1 天开始,根据上面的状态转移方程进行进行动态规划,直到计算出第 n−1 天的结果。

int maxProfit(vector<int>& prices) {
    if (prices.empty()) {
        return 0;
    }

    int n = prices.size();
    // f[i][0]: 手上持有股票的最大收益
    // f[i][1]: 手上不持有股票,并且处于冷冻期中的累计最大收益
    // f[i][2]: 手上不持有股票,并且不在冷冻期中的累计最大收益
    vector<vector<int>> f(n, vector<int>(3));
    f[0][0] = -prices[0];
    for (int i = 1; i < n; ++i) {
        f[i][0] = max(f[i - 1][0], f[i - 1][2] - prices[i]);
        f[i][1] = f[i - 1][0] + prices[i];
        f[i][2] = max(f[i - 1][1], f[i - 1][2]);
    }
    return max(f[n - 1][1], f[n - 1][2]);
}

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

给定一个整数数组 prices,其中 prices[i] 表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。 你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

解法:动态规划

本题和 买卖股票的最佳时机 II 是非常类似的题,唯一的区别就在于本题有「手续费」。在 题目II 的基础之上,dp[i][0]的状态转移方程中减去手续费即可。考虑到「不能同时参与多笔交易」,因此每天交易结束后只可能存在手里有一支股票或者没有股票的状态。

int maxProfit(vector<int>& prices, int fee) {
    int n = prices.size();
    vector<vector<int>> dp(n, vector<int>(2));
    dp[0][0] = 0, dp[0][1] = -prices[0];
    for (int i = 1; i < n; ++i) {
        dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee);
        dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
    }
    return dp[n - 1][0];
}

参考资料:

链接:https://leetcode-cn.com/probl...

链接:https://leetcode-cn.com/probl...

链接:https://leetcode-cn.com/probl...

链接:https://leetcode-cn.com/probl...

链接:https://leetcode-cn.com/probl...

链接:https://leetcode-cn.com/probl...


donnytab
1 声望0 粉丝