【动态规划】股票买卖问题

股票买卖问题

简介

在力扣上,股票买卖问题都可以通过动态规划求解,具体的题目如下:

序号 题目 区别
1 121. 买卖股票的最佳时机 只能交易一次
2 122. 买卖股票的最佳时机 II 交易次数不受限制
3 123. 买卖股票的最佳时机 III 交易次数受限制
4 188. 买卖股票的最佳时机 IV 交易次数为K(可能受限,也可能无限制)
5 309. 最佳买卖股票时机含冷冻期 交易次数不受限制,但包含冷冻期
6 714. 买卖股票的最佳时机含手续费 交易次数不受限制,但包含手续费

一种常用的方法是将「买入」和「卖出」分开进行考虑:「买入」为负收益,而「卖出」为正收益。在初入股市时,你只有「买入」的权利,只能获得负收益。而当你「买入」之后,你就有了「卖出」的权利,可以获得正收益。

显然,我们需要尽可能地降低负收益而提高正收益,因此我们的目标总是将收益值最大化。因此,我们可以使用动态规划的方法,维护在股市中每一天结束后可以获得的「累计最大收益」,并以此进行状态转移,得到最终的答案。

应用

应用1:Leetcode.121

题目

121. 买卖股票的最佳时机

分析

\(dp[i]\) 表示第 \(i\) 天能获取的最大利润。

边界条件

未开始交易的时候,收益为零,即

\[dp[0] = 0 \]

状态转移

显然,要获取最大的收益,我们首先想到的是在最低点买入,然后在最高点卖出,这样,我们就可以获取最大收益。

我们假设前 \(i-1\) 天的股票的最低价格是 \(price_{min}\),由于,肯定要买入股票才会有收益,所以,在第 \(i\) 天,对于当前已经买入的股票,我们有两种选择:

  • 保留股票,收益与前一天的收益相同,即 \(dp[i - 1]\)

  • 卖出股票,收益就是当天的价格与历史最低价格之差,即 $price[i] - price_{min} $;

所以,在第 \(i\) 天的收益,就是上述两种选择的最大值,那么状态转移方程就是:

\[dp[i] = \max(dp[i-1], prices[i] - price_{min}) \]

代码实现

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int minPrice = prices[0];
        int[] dp = new int[n];
        int result = dp[0];
        for (int i = 1; i < n; i++) {
            minPrice = Math.min(minPrice, prices[i]);
            dp[i] = Math.max(dp[i - 1], prices[i] - minPrice);
            result = Math.max(dp[i], result);
        }
        return result;
    }
}

应用2:Leetcode.122

题目

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

分析

\(dp[i][0]\) 表示第 \(i\) 天不持有股票的最大利润,\(dp[i][1]\) 表示第 \(i\) 天持有股票的最大利润,设数组 \(prices\) 的长度为 \(n\) ,那么,在最后一天结束,手上不持有股票,就是最大收益,即 \(dp[n][0]\)

边界条件

显然,第零天不持有股票,最大收益为零,第零天持有股票,但是由于没有卖出,所以,最大收益为负无穷,即:

\[\begin{aligned} & dp[0][0] = 0 \\ & dp[0][1] = -\infty \end{aligned} \]

状态转移

那么,在第 \(i\) 天结束,存在两种情况:

  • 手上持有股票

    • 在第 \(i-1\) 天,就已经持有有股票,第 \(i\) 天没有买入,收益与前一天的收益相同,即 \(dp[i-1][1]\)

    • 在第 \(i-1\) 天,未持有股票,在第 \(i\) 天买入股票,收益为第 \(i-1\) 天的收益与第 \(i\) 天买入的成本之差,即 \(dp[i-1][0] - prices[i-1]\)

    因此,在第 \(i\) 天结束后,若手上持有股票,那么最大收益,就是上述两种情况的最大值,所以,状态转移方程为:

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

  • 手上不持有股票

    • 在第 \(i - 1\) 天手上就已经持有股票,在第 \(i\) 天结束卖出,那么收益为前一天的收益与当天卖出的股票价格之和,即 \(dp[i - 1][1] + prices[i - 1]\)

    • 在第 \(i - 1\) 天结束就已经卖出,第 \(i\) 没有进行交易,那么收益,与前一天的收益相同,即 \(dp[i - 1][0]\)

    因此,在第 \(i\) 天结束后,若手上不持有股票,那么最大收益,就是上述两种情况的最大值,所以,状态转移方程为:

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

所以,综上,状态转移方程为:

\[\begin{aligned} dp[i][1] = \max(dp[i-1][1], dp[i-1][0] - prices[i-1])\\ dp[i][0] = \max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1]) \end{aligned} \]

注意,第 \(i\) 天的股票价格是 \(prices[i - 1]\)

代码实现

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int[][] dp = new int[n + 1][2];

        dp[0][1] = Integer.MIN_VALUE;
        dp[0][0] = 0;
        for (int i = 1; i <= n; i++) {
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1]);
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i - 1]);
        }
        return dp[n][0];
    }
}

应用3:Leetcode.123

题目

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

分析

\(dp[i][k][0]\) 表示最大交易次数为 \(k\) ,第 \(i\) 天结束了,手上持不有股票的最大收益;\(dp[i][k][1]\) 表示最大交易次数为 \(k\) ,第 \(i\) 天结束了,手上持有股票的最大收益。

边界条件

显然,不管最大交易次数为多少次,第零天不持有股票,最大收益为零,第零天持有股票,但是由于没有卖出,所以,最大收益为负无穷,即:

\[\begin{aligned} & dp[0][i][0] = 0, 0 \le i \le K\\ & dp[0][i][1] = -\infty, 0 \le i \le K \end{aligned} \]

状态转移

那么,假设最大交易次数限制\(k\)买入的限制),在第 \(i\) 天结束,存在两种情况:

  • 手上持有股票

    • 在第 \(i-1\) 天,就已经持有有股票,第 \(i\) 天没有买入,收益与前一天的收益相同,即 \(dp[i - 1][k][1]\)

    • 在第 \(i-1\) 天,未持有股票,在第 \(i\) 天买入股票,收益为第 \(i-1\) 天的收益与第 \(i\) 天买入的成本之差,即 \(dp[i-1][k - 1][0] - prices[i - 1]\)

    因此,在第 \(i\) 天结束后,若手上持有股票,那么最大收益,就是上述两种情况的最大值,所以,状态转移方程为:

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

  • 手上不持有股票

    • 在第 \(i - 1\) 天手上就已经持有股票,在第 \(i\) 天结束卖出,那么收益为前一天的收益与当天卖出的股票价格之和,也就是说,在第 \(i-1\)天结束前,就已经进行了第 \(k\) 次买入,最后一天将股票卖出,完成第 \(k\) 次交易,即 \(dp[i - 1][k][1] + prices[i - 1]\)

    • 在第 \(i - 1\) 天结束就已经卖出,第 \(i\) 没有进行交易,那么收益,与前一天的收益相同,也就是说,前一天就已经完成了 \(k\) 次交易,即 \(dp[i - 1][k][0]\)

    因此,在第 \(i\) 天结束后,若手上不持有股票,那么最大收益,就是上述两种情况的最大值,所以,状态转移方程为:

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

所以,综上,状态转移方程为:

\[\begin{aligned} &dp[i][k][0] = max(dp[i - 1][k][0], \ dp[i - 1][k][1] + prices[i - 1]), 1 \le i \le n,\ 1 \le k \le K\\ &dp[i][k][1] = max(dp[i - 1][k][1], \ dp[i - 1][k - 1][0] - prices[i - 1]), 1 \le i \le n,\ 1 \le k \le K \end{aligned} \]

对于本题,我们令 \(k = 2\) 即可,并依次枚举 \(k = 1, 2\) 的场景即可。

代码实现

class Solution {
    public int maxProfit(int[] prices) {
        return maxProfit(prices, 2);
    }

    private int maxProfit(int [] prices, int k) {
        int n = prices.length;
        int [][][] dp = new int [n + 1][k + 1][2];
        for (int i = 0; i <= k; i++) {
            dp[0][i][0] = 0;
            dp[0][i][1] = Integer.MIN_VALUE;
        }

        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= k; j++) {
                dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i - 1]);
                dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i - 1]);
            }
        }
        return dp[n][k][0];
    }
}

应用4:Leetcode.188

题目

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

分析

假设数组 \(prices\) 的长度为 \(n\) ,即天数为 \(n\) ,容易看出,一次交易包含买入和卖出,至少需要两天,也就是说,有效的限制 \(k\) 需要满足 \(k \le \frac{n}{2}\)。否则,就没有约束作用了,相当于没有限制了。

所以,按照 \(K\) 是否有效两种情况,这里直接套用前面的模板即可。

代码实现

class Solution:
    def maxProfit(self, k: int, prices: List[int]) -> int:
        if k == 0:
            return 0

        if k > len(prices) // 2:
            return self.maxProfit1(prices)
        else:
            return self.maxProfit2(k, prices)

    def maxProfit1(self, prices: List[int]) -> int:
        n = len(prices)
        dp = [[0 for _ in range(2)] for _ in range(n + 1)]
        dp[0][0] = 0
        dp[0][1] = -float("inf")

        for i in range(1, n + 1):
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1])
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i - 1])
        return dp[-1][0]

    def maxProfit2(self, K: int, prices: List[int]) -> int:
        n = len(prices)
        dp = [[[0 for _ in range(2)] for _ in range(K + 1)] for _ in range(n + 1)]
        for _k in range(1, K + 1):
            dp[0][_k][0] = 0
            dp[0][_k][1] = -float("inf")

        for i in range(1, n + 1):
            for _k in range(K, 0, -1):
                dp[i][_k][0] = max(dp[i - 1][_k][0], dp[i - 1][_k][1] + prices[i - 1])
                dp[i][_k][1] = max(dp[i - 1][_k][1], dp[i - 1][_k - 1][0] - prices[i - 1])

        return dp[-1][K][0]

应用5:Leetcode.309

题目

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

分析

\(dp[i][0]\) 表示第 \(i\) 天不持有股票的最大利润,\(dp[i][1]\) 表示第 \(i\) 天持有股票的最大利润,设数组 \(prices\) 的长度为 \(n\) ,那么,在最后一天结束,手上不持有股票,就是最大收益,即 \(dp[n][0]\)

注意,这里的「处于冷冻期」指的是在第 \(i\) 天结束之后的状态。也就是说:如果第 \(i\) 天结束之后处于冷冻期,那么第 \(i + 1\) 天无法买入股票。

边界条件

显然,第零天不持有股票,最大收益为零,第零天持有股票,但是由于没有卖出,所以,最大收益为负无穷,即:

\[\begin{aligned} & dp[0][0] = 0 \\ & dp[0][1] = -\infty \end{aligned} \]

状态转移

由于股票买卖需要间隔一天作为冷冻期,所以从卖出的状态转移时,中间要间隔一天。

那么,在第 \(i\) 天结束,存在两种情况:

  • 手上持有股票

    • 在第 \(i-1\) 天,就已经持有有股票,第 \(i\) 天没有买入,收益与前一天的收益相同,即 \(dp[i-1][1]\)
    • 在第 \(i-2\) 天,不持有股票,在第 \(i\) 天买入股票,收益为第 \(i-2\) 天的收益与第 \(i\) 天买入的成本之差,即 \(dp[i-2][0] - prices[i-1]\)
      因为,如果要第 \(i\) 天买入股票,那么,我们就要保证第 \(i - 1\) 天手上不持有股票,且不处于冷冻期,因此,必须要保证第 \(i-2\) 天,就不持有股票。

    综上,在第 \(i\) 天结束后,若手上持有股票,那么最大收益,就是上述两种情况的最大值,所以,状态转移方程为:

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

  • 手上不持有股票

    • 在第 \(i - 1\) 天手上就已经持有股票,在第 \(i\) 天结束卖出,那么收益为第 \(i - 1\) 天的收益与当天卖出的股票价格之和,即 \(dp[i - 1][1] + prices[i - 1]\)
    • 在第 \(i - 1\) 天结束就已经卖出,第 \(i\) 没有进行交易,那么收益,与第 \(i - 1\) 天的收益相同,即 \(dp[i - 1][0]\)

    综上,在第 \(i\) 天结束后,若手上不持有股票,那么最大收益,就是上述两种情况的最大值,所以,状态转移方程为:

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

所以,综上,状态转移方程为:

\[\begin{aligned} dp[i][1] = \max(dp[i - 1][1], dp[i - 2][0] - prices[i - 1])\\ dp[i][0] = \max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1]) \end{aligned} \]

代码实现

from typing import List

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n = len(prices)
        dp = [[0 for _ in range(2)] for i in range(n + 1)]
        dp[0][0] = 0
        dp[0][1] = -float("inf")

        for i in range(1, n + 1):
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1])
            dp[i][1] = max(dp[i - 1][1], dp[i - 2][0] - prices[i - 1])

        return dp[n][0]

应用6:Leetcode.714

题目

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

分析

\(dp[i][0]\) 表示第 \(i\) 天不持有股票的最大利润,\(dp[i][1]\) 表示第 \(i\) 天持有股票的最大利润,设数组 \(prices\) 的长度为 \(n\) ,那么,在最后一天结束,手上不持有股票,就是最大收益,即 \(dp[n][0]\)

边界条件

显然,第零天不持有股票,最大收益为零,第零天持有股票,但是由于没有卖出,所以,最大收益为负无穷,即:

\[\begin{aligned} & dp[0][0] = 0 \\ & dp[0][1] = -\infty \end{aligned} \]

状态转移

那么,在第 \(i\) 天结束,存在两种情况:

  • 手上持有股票

    • 在第 \(i-1\) 天,就已经持有有股票,第 \(i\) 天没有买入,收益与前一天的收益相同,即 \(dp[i-1][1]\)
    • 在第 \(i-1\) 天,不持有股票,在第 \(i\) 天买入股票,收益为第 \(i-1\) 天的收益与第 \(i\) 天买入的成本之差,即 \(dp[i-1][0] - prices[i-1]\)

    综上,在第 \(i\) 天结束后,若手上持有股票,那么最大收益,就是上述两种情况的最大值,所以,状态转移方程为:

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

  • 手上不持有股票

    • 在第 \(i - 1\) 天,就已经持有股票,第 \(i\) 天结束卖出,那么收益为第 \(i - 1\) 天的收益与当天卖出的股票收益之和,即 \(dp[i - 1][1] + prices[i - 1] - fee\)
      注意,卖出股票的收益等于卖出时股票的价格减去手续费。
    • 在第 \(i - 1\) 天,不持有股票,第 \(i\) 没有进行交易,那么收益,与第 \(i - 1\) 天的收益相同,即 \(dp[i - 1][0]\)

    综上,在第 \(i\) 天结束后,若手上不持有股票,那么最大收益,就是上述两种情况的最大值,所以,状态转移方程为:

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

所以,综上,状态转移方程为:

\[\begin{aligned} & dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1]) \\ & dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i - 1] - fee) \end{aligned} \]

代码实现

class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        n = len(prices)
        dp = [[0 for _ in range(2)] for i in range(n + 1)]
        dp[0][0] = 0
        dp[0][1] = -float("inf")

        for i in range(1, n + 1):
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1]- fee)
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i - 1])
        return dp[-1][0]

参考:

posted @ 2023-01-11 23:02  LARRY1024  阅读(103)  评论(0编辑  收藏  举报