本文地址:https://leetcode-cn.com/circle/article/qiAgHn/web
原文出处:https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/discuss/108870/Most-consistent-ways-of-dealing-with-the-series-of-stock-problems算法
你们好,我是吴师兄,今天给你们分享一篇关于股票问题系列的文章,掌握后轻轻松松秒杀 LeetCode 上全部的股票问题。
数组
本文的做者为 Storm,目前在力扣上全站排名第一,已获做者受权转载此文,但愿对你刷题有帮助。微信

前言
此文为转载翻译,和原文相比,这篇文章多了未优化空间的代码,且代码都从新写了,另外更改了部分文字描述。编辑器
股票问题一共有六道题:flex
-
12一、买卖股票的最佳时机 -
12二、买卖股票的最佳时机 II -
12三、买卖股票的最佳时机 III -
18八、买卖股票的最佳时机 IV -
30九、最佳买卖股票时机含冷冻期 -
71四、买卖股票的最佳时机含手续费
每一个问题都有优质的题解,可是大多数题解没有创建起这些问题之间的联系,也没有给出股票问题系列的通解。优化
这篇文章给出适用于所有股票问题的通解,以及对于每一个特定问题的特解。url
1、通用状况
这个想法基于以下问题:给定一个表示天天股票价格的数组,什么因素决定了能够得到的最大收益?spa
相信大多数人能够很快给出答案,例如「在哪些天进行交易以及容许多少次交易」。.net
这些因素固然重要,在问题描述中也有这些因素。
然而还有一个隐藏可是关键的因素决定了最大收益,下文将阐述这一点。
首先介绍一些符号:
-
用 n 表示股票价格数组的长度; -
用 i 表示第 i 天( i 的取值范围是 0 到 n - 1); -
用 k 表示容许的最大交易次数; -
用 T[i][k] 表示在第 i 天结束时,最多进行 k 次交易的状况下能够得到的最大收益。
基准状况是显而易见的:T[-1][k] = T[i][0] = 0
,表示没有进行股票交易时没有收益(注意第一天对应 i = 0
,所以 i = -1
表示没有股票交易)。
如今若是能够将 T[i][k]
关联到子问题,例如 T[i - 1][k]
、T[i][k - 1]
、T[i - 1][k - 1]
等子问题,就能获得状态转移方程,并对这个问题求解。
如何获得状态转移方程呢?
最直接的办法是看第 i 天可能的操做。有多少个选项?答案是三个:买入、卖出、休息。
应该选择哪一个操做?
答案是:并不知道哪一个操做是最好的,可是能够经过计算获得选择每一个操做能够获得的最大收益。
假设没有别的限制条件,则能够尝试每一种操做,并选择能够最大化收益的一种操做。
可是,题目中确实有限制条件,规定不能同时进行屡次交易,所以若是决定在第 i 天买入,在买入以前必须持有 0 份股票,若是决定在第 i 天卖出,在卖出以前必须刚好持有 1 份股票。
持有股票的数量是上文说起到的隐藏因素,该因素影响第 i 天能够进行的操做,进而影响最大收益。
所以对 T[i][k]
的定义须要分红两项:
-
T[i][k][0]
表示在第 i 天结束时,最多进行 k 次交易且在进行操做后持有 0 份股票的状况下能够得到的最大收益; -
T[i][k][1]
表示在第 i 天结束时,最多进行 k 次交易且在进行操做后持有 1 份股票的状况下能够得到的最大收益。使用新的状态表示以后,能够获得基准状况和状态转移方程。
基准状况:
T[-1][k][0] = 0, T[-1][k][1] = -Infinity
T[i][0][0] = 0, T[i][0][1] = -Infinity
状态转移方程:
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])
基准状况中,T[-1][k][0] = T[i][0][0] = 0
的含义和上文相同,T[-1][k][1] = T[i][0][1] = -Infinity
的含义是在没有进行股票交易时不容许持有股票。
对于状态转移方程中的 T[i][k][0]
,第 i 天进行的操做只能是休息或卖出,由于在第 i 天结束时持有的股票数量是 0。T[i - 1][k][0]
是休息操做能够获得的最大收益,T[i - 1][k][1] + prices[i]
是卖出操做能够获得的最大收益。
注意到容许的最大交易次数是不变的,由于每次交易包含两次成对的操做,买入和卖出。
只有买入操做会改变容许的最大交易次数。
对于状态转移方程中的 T[i][k][1]
,第 i 天进行的操做只能是休息或买入,由于在第 i 天结束时持有的股票数量是 1。T[i - 1][k][1]
是休息操做能够获得的最大收益,T[i - 1][k - 1][0] - prices[i]
是买入操做能够获得的最大收益。
注意到容许的最大交易次数减小了一次,由于每次买入操做会使用一次交易。
为了获得最后一天结束时的最大收益,能够遍历股票价格数组,根据状态转移方程计算 T[i][k][0]
和 T[i][k][1]
的值。最终答案是 T[n - 1][k][0]
,由于结束时持有 0 份股票的收益必定大于持有 1 份股票的收益。
2、应用于特殊状况
上述六个股票问题是根据 k 的值进行分类的,其中 k 是容许的最大交易次数。最后两个问题有附加限制,包括「冷冻期」和「手续费」。通解能够应用于每一个股票问题。
状况一:k = 1
状况一对应的题目是「121. 买卖股票的最佳时机」。
对于状况一,天天有两个未知变量: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];
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;
}
}
如今对上述解法进行分析。对于循环中的部分,profit1 实际上只是表示到第 i 天的股票价格的相反数中的最大值,或者等价地表示到第 i 天的股票价格的最小值。对于 profit0,只须要决定卖出和休息中的哪项操做能够获得更高的收益。若是进行卖出操做,则买入股票的价格为 profit1,即第 i 天以前(不含第 i 天)的最低股票价格。
这正是现实中为了得到最大收益会作的事情。可是这种作法不是惟一适用于这种状况的解决方案。
状况二:k 为正无穷
状况二对应的题目是「122. 买卖股票的最佳时机 II」。
若是 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;
}
}
这个解法提供了得到最大收益的贪心策略:可能的状况下,在每一个局部最小值买入股票,而后在以后遇到的第一个局部最大值卖出股票。这个作法等价于找到股票价格数组中的递增子数组,对于每一个递增子数组,在开始位置买入并在结束位置卖出。
能够看到,这和累计收益是相同的,只要这样的操做的收益为正。
状况三:k = 2
状况三对应的题目是「123. 买卖股票的最佳时机 III」。
状况三和状况一类似,区别之处是,对于状况三,天天有四个未知变量: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;
}
}
状况四:k 为任意值
状况四对应的题目是「188. 买卖股票的最佳时机 IV」。
状况四是最通用的状况,对于每一天须要使用不一样的 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 的值很大的时候会超出时间限制。
状况五:k 为正无穷但有冷却时间
状况五对应的题目是「309. 最佳买卖股票时机含冷冻期」。
因为具备相同的 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;
}
}
状况六:k 为正无穷但有手续费
状况六对应的题目是「714. 买卖股票的最佳时机含手续费」。
因为具备相同的 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) 的解法。
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)。
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;
}
}
3、总结
总而言之,股票问题最通用的状况由三个特征决定:当前的天数 i、容许的最大交易次数 k 以及天天结束时持有的股票数。
这篇文章阐述了最大利润的状态转移方程和终止条件,由此能够获得时间复杂度为 O(nk) 和空间复杂度为 O(k) 的解法。
该解法能够应用于六个问题,对于最后两个问题,须要将状态转移方程进行一些修改。
本文分享自微信公众号 - 五分钟学算法(CXYxiaowu)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。