【动态规划】股票买卖问题
股票买卖问题
简介
在力扣上,股票买卖问题都可以通过动态规划求解,具体的题目如下:
序号 | 题目 | 区别 |
---|---|---|
1 | 121. 买卖股票的最佳时机 | 只能交易一次 |
2 | 122. 买卖股票的最佳时机 II | 交易次数不受限制 |
3 | 123. 买卖股票的最佳时机 III | 交易次数受限制 |
4 | 188. 买卖股票的最佳时机 IV | 交易次数为K(可能受限,也可能无限制) |
5 | 309. 最佳买卖股票时机含冷冻期 | 交易次数不受限制,但包含冷冻期 |
6 | 714. 买卖股票的最佳时机含手续费 | 交易次数不受限制,但包含手续费 |
一种常用的方法是将「买入」和「卖出」分开进行考虑:「买入」为负收益,而「卖出」为正收益。在初入股市时,你只有「买入」的权利,只能获得负收益。而当你「买入」之后,你就有了「卖出」的权利,可以获得正收益。
显然,我们需要尽可能地降低负收益而提高正收益,因此我们的目标总是将收益值最大化。因此,我们可以使用动态规划的方法,维护在股市中每一天结束后可以获得的「累计最大收益」,并以此进行状态转移,得到最终的答案。
应用
应用1:Leetcode.121
题目
分析
设 \(dp[i]\) 表示第 \(i\) 天能获取的最大利润。
边界条件
未开始交易的时候,收益为零,即
状态转移
显然,要获取最大的收益,我们首先想到的是在最低点买入,然后在最高点卖出,这样,我们就可以获取最大收益。
我们假设前 \(i-1\) 天的股票的最低价格是 \(price_{min}\),由于,肯定要买入股票才会有收益,所以,在第 \(i\) 天,对于当前已经买入的股票,我们有两种选择:
-
保留股票,收益与前一天的收益相同,即 \(dp[i - 1]\);
-
卖出股票,收益就是当天的价格与历史最低价格之差,即 $price[i] - price_{min} $;
所以,在第 \(i\) 天的收益,就是上述两种选择的最大值,那么状态转移方程就是:
代码实现
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
题目
分析
设 \(dp[i][0]\) 表示第 \(i\) 天不持有股票的最大利润,\(dp[i][1]\) 表示第 \(i\) 天持有股票的最大利润,设数组 \(prices\) 的长度为 \(n\) ,那么,在最后一天结束,手上不持有股票,就是最大收益,即 \(dp[n][0]\) 。
边界条件
显然,第零天不持有股票,最大收益为零,第零天持有股票,但是由于没有卖出,所以,最大收益为负无穷,即:
状态转移
那么,在第 \(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]) \] -
所以,综上,状态转移方程为:
注意,第 \(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
题目
分析
设 \(dp[i][k][0]\) 表示最大交易次数为 \(k\) ,第 \(i\) 天结束了,手上持不有股票的最大收益;\(dp[i][k][1]\) 表示最大交易次数为 \(k\) ,第 \(i\) 天结束了,手上持有股票的最大收益。
边界条件
显然,不管最大交易次数为多少次,第零天不持有股票,最大收益为零,第零天持有股票,但是由于没有卖出,所以,最大收益为负无穷,即:
状态转移
那么,假设最大交易次数限制为 \(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]) \] -
所以,综上,状态转移方程为:
对于本题,我们令 \(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
题目
分析
假设数组 \(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
题目
分析
设 \(dp[i][0]\) 表示第 \(i\) 天不持有股票的最大利润,\(dp[i][1]\) 表示第 \(i\) 天持有股票的最大利润,设数组 \(prices\) 的长度为 \(n\) ,那么,在最后一天结束,手上不持有股票,就是最大收益,即 \(dp[n][0]\) 。
注意,这里的「处于冷冻期」指的是在第 \(i\) 天结束之后的状态。也就是说:如果第 \(i\) 天结束之后处于冷冻期,那么第 \(i + 1\) 天无法买入股票。
边界条件
显然,第零天不持有股票,最大收益为零,第零天持有股票,但是由于没有卖出,所以,最大收益为负无穷,即:
状态转移
由于股票买卖需要间隔一天作为冷冻期,所以从卖出的状态转移时,中间要间隔一天。
那么,在第 \(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]) \]
所以,综上,状态转移方程为:
代码实现
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
题目
分析
设 \(dp[i][0]\) 表示第 \(i\) 天不持有股票的最大利润,\(dp[i][1]\) 表示第 \(i\) 天持有股票的最大利润,设数组 \(prices\) 的长度为 \(n\) ,那么,在最后一天结束,手上不持有股票,就是最大收益,即 \(dp[n][0]\) 。
边界条件
显然,第零天不持有股票,最大收益为零,第零天持有股票,但是由于没有卖出,所以,最大收益为负无穷,即:
状态转移
那么,在第 \(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) \] - 在第 \(i - 1\) 天,就已经持有股票,第 \(i\) 天结束卖出,那么收益为第 \(i - 1\) 天的收益与当天卖出的股票收益之和,即 \(dp[i - 1][1] + prices[i - 1] - fee\);
所以,综上,状态转移方程为:
代码实现
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]
参考: