LeetCode题解:股票问题
自大学开始,我便陆陆续续的学习一些 算法和数据结构 方面的内容,同时也开始在一些平台刷题,也会参加一些大大小小的算法竞赛。但是平时刷题缺少目的性、系统性,最终导致算法方面进步缓慢。最终,为了自己的未来,我决定开始在LeetCode上进行系统的学习和练习,同时将刷题的轨迹整理记录,分享出来与大家共勉。
参考教材: labuladong的算法小抄官方完整版
参考资料: 股票问题系列通解
题目列表: https://leetcode-cn.com/list/x87xoxmg
目录标题
- LeetCode题解:股票问题
- 000.算法框架
- [121. 买卖股票的最佳时机](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/)
- [122. 买卖股票的最佳时机 II](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/)
- [123. 买卖股票的最佳时机 III](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/)
- [188. 买卖股票的最佳时机 IV](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/)
- [309. 最佳买卖股票时机含冷冻期](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/)
- [714. 买卖股票的最佳时机含手续费](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/)
000.算法框架
注意: 文章内容基于参考资料进行整理,整理的比较简单,这里还是建议 查看原文 进行学习。
用一个三维数组就可以装下这几种状态的全部组合:
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)
而且我们可以⽤⾃然语⾔描述出每⼀个状态的含义,⽐如说 dp[3][2][1] 的含义就是:今天是第三天,我现在⼿上持有着股票,⾄今最多进行 2 次交 易。再⽐如 dp[2][3][0] 的含义:今天是第⼆天,我现在⼿上没有持有股 票,⾄今最多进⾏ 3 次交易。
想求的最终答案是 dp[n - 1][K][0], 即最后⼀天, 最多允许 K 次交易,最多获得多少利润。
状态转移方程:
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, 所以今天我就持有股票了。
定义 base case , 即最简单的情况。
dp[-1][k][0] = 0
解释: 因为 i 是从 0 开始的, 所以 i = -1 意味着还没有开始, 这时候的利润当然是 0。
dp[-1][k][1] = -infinity
解释: 还没开始的时候, 是不可能持有股票的, ⽤负⽆穷表⽰这种不可能。
dp[i][0][0] = 0
解释: 因为 k 是从 1 开始的, 所以 k = 0 意味着根本不允许交易, 这时候利润当然是 0。
dp[i][0][1] = -infinity
解释: 不允许交易的情况下, 是不可能持有股票的, ⽤负⽆穷表⽰这种不可能。
把上⾯的状态转移方程总结⼀下:
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])
本文的六个股票问题是根据 k 的值进行分类的,其中 k 是允许的最大交易次数。最后两个问题有附加限制,包括「冷冻期」和「手续费」。通解可以应用于每个股票问题。
121. 买卖股票的最佳时机
难度: 简单
给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0
。
示例:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
题解:
情况一:k = 1
对于情况一,每天有两个未知变量:T[i][1][0] 和 T[i][1][1],状态转移方程如下:
T[i][1][0] = max(T[i - 1][1][0], T[i - 1][1][1] + prices[i])
T[i][1][1] = max(T[i - 1][1][1], T[i - 1][0][0] - prices[i]) = max(T[i - 1][1][1], -prices[i])
第二个状态转移方程利用了 T[i][0][0] = 0。
根据上述状态转移方程,可以写出时间复杂度为 O(n) 和空间复杂度为 O(n) 的解法。
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int length = prices.length;
int[][] dp = new int[length][2];
//i=0时,dp[i-1]不合法,预处理
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < length; i++) {
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[length - 1][0];
}
}
如果注意到第 i 天的最大收益只和第 i - 1 天的最大收益相关,空间复杂度可以降到 O(1)。
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int profit0 = 0, profit1 = -prices[0];
int length = prices.length;
for (int i = 1; i < length; i++) {
profit0 = Math.max(profit0, profit1 + prices[i]);
profit1 = Math.max(profit1, -prices[i]);
}
return profit0;
}
}
122. 买卖股票的最佳时机 II
难度: 简单
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例:
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
题解:
情况二:k 为正无穷
如果 k 为正无穷,则 k 和 k - 1 可以看成是相同的,因此有 T[i - 1][k - 1][0] = T[i - 1][k][0] 和 T[i - 1][k - 1][1] = T[i - 1][k][1]。每天仍有两个未知变量:T[i][k][0] 和 T[i][k][1],其中 k 为正无穷,状态转移方程如下:
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 1][k - 1][0] - prices[i]) = max(T[i - 1][k][1], T[i - 1][k][0] - prices[i])
第二个状态转移方程利用了 T[i - 1][k - 1][0] = T[i - 1][k][0]。
根据上述状态转移方程,可以写出时间复杂度为 O(n) 和空间复杂度为 O(n) 的解法。
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int length = prices.length;
int[][] dp = new int[length][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < length; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
}
return dp[length - 1][0];
}
}
如果注意到第 i 天的最大收益只和第 i - 1 天的最大收益相关,空间复杂度可以降到 O(1)。
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int profit0 = 0, profit1 = -prices[0];
int length = prices.length;
for (int i = 1; i < length; i++) {
int newProfit0 = Math.max(profit0, profit1 + prices[i]);
int newProfit1 = Math.max(profit1, profit0 - prices[i]);
profit0 = newProfit0;
profit1 = newProfit1;
}
return profit0;
}
}
这个解法提供了获得最大收益的贪心策略:可能的情况下,在每个局部最小值买入股票,然后在之后遇到的第一个局部最大值卖出股票。这个做法等价于找到股票价格数组中的递增子数组,对于每个递增子数组,在开始位置买入并在结束位置卖出。可以看到,这和累计收益是相同的,只要这样的操作的收益为正。
123. 买卖股票的最佳时机 III
难度:困难
给定一个数组,它的第 i
个元素是一支给定的股票在第 i
天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例:
输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
情况三:k = 2
情况三和情况一相似,区别之处是,对于情况三,每天有四个未知变量:T[i][1][0]、T[i][1][1]、T[i][2][0]、T[i][2][1],
状态转移方程如下:
T[i][2][0] = max(T[i - 1][2][0], T[i - 1][2][1] + prices[i])
T[i][2][1] = max(T[i - 1][2][1], T[i - 1][1][0] - prices[i])
T[i][1][0] = max(T[i - 1][1][0], T[i - 1][1][1] + prices[i])
T[i][1][1] = max(T[i - 1][1][1], T[i - 1][0][0] - prices[i]) = max(T[i - 1][1][1], -prices[i])
第四个状态转移方程利用了 T[i][0][0] = 0。
根据上述状态转移方程,可以写出时间复杂度为 O(n) 和空间复杂度为 O(n) 的解法。
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int length = prices.length;
int[][][] dp = new int[length][3][2];
dp[0][1][0] = 0;
dp[0][1][1] = -prices[0];
dp[0][2][0] = 0;
dp[0][2][1] = -prices[0];
for (int i = 1; i < length; i++) {
dp[i][2][0] = Math.max(dp[i - 1][2][0], dp[i - 1][2][1] + prices[i]);
dp[i][2][1] = Math.max(dp[i - 1][2][1], dp[i - 1][1][0] - prices[i]);
dp[i][1][0] = Math.max(dp[i - 1][1][0], dp[i - 1][1][1] + prices[i]);
dp[i][1][1] = Math.max(dp[i - 1][1][1], dp[i - 1][0][0] - prices[i]);
}
return dp[length - 1][2][0];
}
}
如果注意到第 i 天的最大收益只和第 i - 1 天的最大收益相关,空间复杂度可以降到 O(1)。
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int profitOne0 = 0, profitOne1 = -prices[0], profitTwo0 = 0, profitTwo1 = -prices[0];
int length = prices.length;
for (int i = 1; i < length; i++) {
profitTwo0 = Math.max(profitTwo0, profitTwo1 + prices[i]);
profitTwo1 = Math.max(profitTwo1, profitOne0 - prices[i]);
profitOne0 = Math.max(profitOne0, profitOne1 + prices[i]);
profitOne1 = Math.max(profitOne1, -prices[i]);
}
return profitTwo0;
}
}
188. 买卖股票的最佳时机 IV
难度: 困难
给定一个整数数组 prices
,它的第 i
个元素 prices[i]
是一支给定的股票在第 i
天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例:
输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
情况四:k 为任意值
情况四是最通用的情况,对于每一天需要使用不同的 k 值更新所有的最大收益,对应持有 0 份股票或 1 份股票。如果 k 超过一个临界值,最大收益就不再取决于允许的最大交易次数,而是取决于股票价格数组的长度,因此可以进行优化。那么这个临界值是什么呢?
一个有收益的交易至少需要两天(在前一天买入,在后一天卖出,前提是买入价格低于卖出价格)。如果股票价格数组的长度为 n,则有收益的交易的数量最多为 n / 2(整数除法)。因此 k 的临界值是 n / 2。如果给定的 k 不小于临界值,即 k >= n / 2,则可以将 k 扩展为正无穷,此时问题等价于情况二。
根据状态转移方程,可以写出时间复杂度为 O(nk)和空间复杂度为 O(nk) 的解法。
class Solution {
public int maxProfit(int k, int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int length = prices.length;
if (k >= length / 2) {
return maxProfit(prices);
}
int[][][] dp = new int[length][k + 1][2];
for (int i = 1; i <= k; i++) {
dp[0][i][0] = 0;
dp[0][i][1] = -prices[0];
}
for (int i = 1; i < length; i++) {
for (int j = k; j > 0; j--) {
dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i]);
dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]);
}
}
return dp[length - 1][k][0];
}
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int length = prices.length;
int[][] dp = new int[length][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < length; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
}
return dp[length - 1][0];
}
}
如果注意到第 i 天的最大收益只和第 i - 1 天的最大收益相关,空间复杂度可以降到 O(k)。
class Solution {
public int maxProfit(int k, int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int length = prices.length;
if (k >= length / 2) {
return maxProfit(prices);
}
int[][] dp = new int[k + 1][2];
for (int i = 1; i <= k; i++) {
dp[i][0] = 0;
dp[i][1] = -prices[0];
}
for (int i = 1; i < length; i++) {
for (int j = k; j > 0; j--) {
dp[j][0] = Math.max(dp[j][0], dp[j][1] + prices[i]);
dp[j][1] = Math.max(dp[j][1], dp[j - 1][0] - prices[i]);
}
}
return dp[k][0];
}
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int profit0 = 0, profit1 = -prices[0];
int length = prices.length;
for (int i = 1; i < length; i++) {
int newProfit0 = Math.max(profit0, profit1 + prices[i]);
int newProfit1 = Math.max(profit1, profit0 - prices[i]);
profit0 = newProfit0;
profit1 = newProfit1;
}
return profit0;
}
}
如果不根据 k 的值进行优化,在 k 的值很大的时候会超出时间限制。
309. 最佳买卖股票时机含冷冻期
难度: 中等
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
- 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
- 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
示例:
输入: [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
情况五:k 为正无穷但有冷却时间
由于具有相同的 k 值,因此情况五和情况二非常相似,不同之处在于情况五有「冷却时间」的限制,因此需要对状态转移方程进行一些修改。
情况二的状态转移方程如下:
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 1][k][0] - prices[i])
但是在有「冷却时间」的情况下,如果在第 i - 1 天卖出了股票,就不能在第 i 天买入股票。因此,如果要在第 i 天买入股票,第二个状态转移方程中就不能使用 T[i - 1][k][0],而应该使用 T[i - 2][k][0]。状态转移方程中的别的项保持不变,新的状态转移方程如下:
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 2][k][0] - prices[i])
根据上述状态转移方程,可以写出时间复杂度为 O(n) 和空间复杂度为 O(n)的解法。
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int length = prices.length;
int[][] dp = new int[length][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < length; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], (i >= 2 ? dp[i - 2][0] : 0) - prices[i]);
}
return dp[length - 1][0];
}
}
如果注意到第 i 天的最大收益只和第 i - 1 天和第 i - 2 天的最大收益相关,空间复杂度可以降到 O(1)。
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int prevProfit0 = 0, profit0 = 0, profit1 = -prices[0];
int length = prices.length;
for (int i = 1; i < length; i++) {
int nextProfit0 = Math.max(profit0, profit1 + prices[i]);
int nextProfit1 = Math.max(profit1, prevProfit0 - prices[i]);
prevProfit0 = profit0;
profit0 = nextProfit0;
profit1 = nextProfit1;
}
return profit0;
}
}
714. 买卖股票的最佳时机含手续费
难度: 中等
给定一个整数数组 prices
,其中第 i
个元素代表了第 i
天的股票价格 ;非负整数 fee
代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意: 这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
示例:
输入: prices = [1, 3, 2, 8, 4, 9], fee = 2
输出: 8
解释: 能够达到的最大利润:
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8.
情况六:k 为正无穷但有手续费
由于具有相同的 k 值,因此情况六和情况二非常相似,不同之处在于情况六有「手续费」,因此需要对状态转移方程进行一些修改。
情况二的状态转移方程如下:
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 1][k][0] - prices[i])
由于需要对每次交易付手续费,因此在每次买入或卖出股票之后的收益需要扣除手续费,新的状态转移方程有两种表示方法。
第一种表示方法,在每次买入股票时扣除手续费:
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 1][k][0] - prices[i] - fee)
第二种表示方法,在每次卖出股票时扣除手续费:
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i] - fee)
T[i][k][1] = max(T[i - 1][k][1], T[i - 1][k][0] - prices[i])
根据上述状态转移方程,可以写出时间复杂度为 O(n)O(n) 和空间复杂度为 O(n)O(n) 的解法。
class Solution {
public int maxProfit(int[] prices, int fee) {
if (prices == null || prices.length == 0) {
return 0;
}
int length = prices.length;
int[][] dp = new int[length][2];
dp[0][0] = 0;
dp[0][1] = -prices[0] - fee;
for (int i = 1; i < length; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i] - fee);
}
return dp[length - 1][0];
}
}
如果注意到第 i 天的最大收益只和第 i - 1 天的最大收益相关,空间复杂度可以降到 O(1)O(1)。
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int profit0 = 0, profit1 = -prices[0] - fee;
int length = prices.length;
for (int i = 1; i < length; i++) {
int newProfit0 = Math.max(profit0, profit1 + prices[i]);
int newProfit1 = Math.max(profit1, profit0 - prices[i] - fee);
profit0 = newProfit0;
profit1 = newProfit1;
}
return profit0;
}
}
作者:耿鬼不会笑
时间:2021年2月