我们以下面这道题来了解下股票问题这个框架。
具体可参考labuladong
每天都有三种「选择」:买入,卖出,无操作,我们用
buy
,sell
,rest
表示三种选择。
但问题是,并不是每天都可以任意选择这三种选择,因为sell
必须在buy
之后,buy
又必须在sell
之后。那么rest
操作之后还应该分两种状态:一种是buy
之后的rest
(持有了股票),一种是sell
之后的rest
(没有持有股票)。而且别忘了,我们还有交易次数k
的限制,就是说你buy
还只能在k > 0
的前提下操作。
这个问题的「状态」有三个,第一个是天数,第二个是允许交易的最大次数,第三个是当前的持有状态(即之前说的rest
的状态,我们不妨用
1 表示持有,0
表示没有持有)。然后我们用一个三维数组就可以装下这几种状态的全部组合:
1 2 3 4 5 6 7 8 9
| dp[i][k][0 or 1] 0 <= i <= n - 1, 1 <= k <= K n 为天数,大 K 为交易数的上限,0 和 1 代表是否持有股票。 此问题共 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[n - 1][K][1]
?因为dp[n - 1][K][1]
代表到最后一天手上还持有股票,dp[n - 1][K][0]
表示最后一天手上的股票已经卖出去了,很显然后者得到的利润一定大于前者。
记住如何解释「状态」,一旦你觉得哪里不好理解,把它翻译成自然语言就容易理解了。
现在,我们完成了「状态」的穷举,我们开始思考每种「状态」有哪些「选择」,应该如何更新「状态」。
只看「持有状态」,可以画个状态转移图:
通过这个图可以很清楚地看到,每种状态(0 和
1)是如何转移而来的。根据这个图,我们来写一下状态转移方程:
1 2
| dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]) max( 今天选择 rest, 今天选择 sell )
|
解释:今天我没有持有股票,有两种可能,我从这两种可能中求最大利润:
1、我昨天就没有持有,且截至昨天最大交易次数限制为k
;然后我今天选择rest
,所以我今天还是没有持有,最大交易次数限制依然为k
。
2、我昨天持有股票,且截至昨天最大交易次数限制为k
;但是今天我sell
了,所以我今天没有持有股票了,最大交易次数限制依然为k
。
1 2
| dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]) max( 今天选择 rest, 今天选择 buy )
|
解释:今天我持有着股票,最大交易次数限制为k
,那么对于昨天来说,有两种可能,我从这两种可能中求最大利润:
1、我昨天就持有着股票,且截至昨天最大交易次数限制为k
;然后今天选择rest
,所以我今天还持有着股票,最大交易次数限制依然为k
。
2、我昨天本没有持有,且截至昨天最大交易次数限制为k - 1
;但今天我选择buy
,所以今天我就持有股票了,最大交易次数限制为k
。
ps:这里着重提醒一下,时刻牢记「状态」的定义,k
的定义并不是「已进行的交易次数」,而是「最大交易次数的上限限制」。如果确定今天进行一次交易,且要保证截至今天最大交易次数上限为k
,那么昨天的最大交易次数上限必须是k - 1
。
这个解释应该很清楚了,如果buy
,就要从利润中减去prices[i]
,如果sell
,就要给利润增加prices[i]
。今天的最大利润就是这两种可能选择中较大的那个。
注意k
的限制,在选择buy
的时候相当于开启了一次交易,那么对于昨天来说,交易次数的上限k
应该减小
1。
1 2 3 4 5 6 7 8 9 10 11 12 13
| dp[-1][...][0] = 0 解释:因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0。
dp[-1][...][1] = -infinity 解释:还没开始的时候,是不可能持有股票的。 因为我们的算法要求一个最大值,所以初始值设为一个最小值,方便取最大值。
dp[...][0][0] = 0 解释:因为 k 是从 1 开始的,所以 k = 0 意味着根本不允许交易,这时候利润当然是 0。
dp[...][0][1] = -infinity 解释:不允许交易的情况下,是不可能持有股票的。 因为我们的算法要求一个最大值,所以初始值设为一个最小值,方便取最大值。
|
把上面的状态转移方程总结一下:
1 2 3 4 5 6 7
| base case: dp[-1][...][0] = dp[...][0][0] = 0 dp[-1][...][1] = dp[...][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])
|
给定一个数组 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
我们直接套模版,
1 2 3 4 5 6 7 8 9
| dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i]) dp[i][1][1] = max(dp[i-1][1][1], dp[i-1][0][0] - prices[i]) = max(dp[i-1][1][1], -prices[i]) 解释:k = 0 的 base case,所以 dp[i-1][0][0] = 0。
现在发现 k 都是 1,不会改变,即 k 对状态转移已经没有影响了。 可以进行进一步化简去掉所有 k: dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]) dp[i][1] = max(dp[i-1][1], -prices[i])
|
写出如下代码:
1 2 3 4 5 6 7
| int n = prices.length; int[][] dp = new int[n][2]; for (int i = 0; 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], -prices[i]); } return dp[n - 1][0];
|
显然i = 0
时i - 1
是不合法的索引,这是因为我们没有对i
的
base case 进行处理,可以这样给一个特化处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| if (i - 1 == -1) { dp[i][0] = 0;
dp[i][1] = -prices[i]; continue; }
|
所以代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class Solution { public: int maxProfit(vector<int>& prices) { int n = prices.size();
vector<vector<int> > dp(n, vector<int>(2,0));
for(int i = 0; i < n; i++){ if(i - 1 == -1){ dp[i][0] = 0; dp[i][1] = -prices[i]; continue; } dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]); dp[i][1] = max(dp[i - 1][1], -prices[i]); } return dp[n - 1][0]; } };
|
注意一下状态转移方程,新状态只和相邻的一个状态有关,其实不用整个dp
数组,只需要一个变量储存相邻的那个状态就足够了,这样可以把空间复杂度降到
O(1)
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Solution { public: int maxProfit(vector<int>& prices) { int n = prices.size();
int dp_i_0 = 0, dp_i_1 = INT_MIN; for(int i = 0; i < n; i++){ dp_i_0 = max(dp_i_0, dp_i_1 + prices[i]); dp_i_1 = max(dp_i_1, -prices[i]); } return dp_i_0; } };
|
给定一个数组 prices ,其中 prices[i] 表示股票第 i 天的价格。
在每一天,你可能会决定购买和/或出售股票。你在任何时候 最多 只能持有
一股 股票。你也可以购买它,然后在 同一天 出售。 返回 你能获得的 最大
利润 。
示例:
输入: prices = [7,1,5,3,6,4]
输出: 7
解释:
在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 =
5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 =
6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
思路:
相当于k = +infinity
,那么我们可以认为k
和k-1
是一样的,可以这样改写框架:
1 2 3 4 5 6 7
| 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][1], dp[i-1][k-1][0] - prices[i]); = max(dp[i-1][k][1], dp[i-1][k][0] - prices[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]);
|
所以可以写出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class Solution { public: int maxProfit(vector<int>& prices) { int n = prices.size();
vector<vector<int> > dp(n, vector<int>(2,0));
for(int i = 0; i < n; i++){ if(i - 1 == -1){ dp[i][0] = 0; dp[i][1] = -prices[i]; continue; } 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(1)
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Solution { public: int maxProfit(vector<int>& prices) { int n = prices.size();
int dp_i_0 = 0, dp_i_1 = INT_MIN; for(int i = 0; i < n; i++){ dp_i_0 = max(dp_i_0, dp_i_1 + prices[i]); dp_i_1 = max(dp_i_1, dp_i_0 - prices[i]); } return dp_i_0; } };
|
给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例:
输入: prices = [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
思路:
相当于k = infinity +cooldowm
每次sell
之后要等一天才能继续交易。只要把这个特点融入上一题的状态转移方程即可:
1 2
| dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]); dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i]);
|
所以最终可以写成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class Solution { public: int maxProfit(vector<int>& prices) { int n = prices.size();
vector<vector<int> > dp(n, vector<int>(2,0));
for(int i = 0; i < n; i++){ if(i - 1 == -1){ dp[i][0] = 0; dp[i][1] = -prices[i]; continue; } if(i - 2 == -1){ dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]); dp[i][1] = max(dp[i - 1][1], -prices[i]); continue; } dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]); dp[i][1] = max(dp[i - 1][1], dp[i - 2][0] - prices[i]); } return dp[n - 1][0]; } };
|
状态压缩:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Solution { public: int maxProfit(vector<int>& prices) { int n = prices.size();
int dp_i_0 = 0, dp_i_1 = INT_MIN; int dp_pre_0 = 0;
for(int i = 0; i < n; i++){ int temp = dp_i_0; dp_i_0 = max(dp_i_0, dp_i_1 + prices[i]); dp_i_1 = max(dp_i_1, dp_pre_0 - prices[i]); dp_pre_0 = temp; } return dp_i_0; } };
|
给定一个整数数组 prices,其中 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 = infinity + fee
改写方程:
1 2
| 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] - fee);
|
如果直接把fee
放在第一个式子里减,会有测试用例无法通过,错误原因是整型溢出而不是思路问题。一种解决方案是把代码中的int
类型都改成long
类型,避免int
的整型溢出。
所以最终代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class Solution { public: int maxProfit(vector<int>& prices, int fee) { int n = prices.size();
vector<vector<int> > dp(n, vector<int>(2,0));
for(int i = 0; i < n; i++){ if(i - 1 == -1){ dp[i][0] = 0; dp[i][1] = -prices[i] - fee; continue; } 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] - fee); } return dp[n - 1][0]; } };
|
状态压缩如下
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Solution { public: int maxProfit(vector<int>& prices, int fee) { int n = prices.size();
int dp_i_0 = 0, dp_i_1 = INT_MIN; for(int i = 0; i < n; i++){ dp_i_0 = max(dp_i_0, dp_i_1 + prices[i]); dp_i_1 = max(dp_i_1, dp_i_0 - prices[i] - fee); } return dp_i_0; } };
|
给定一个数组,它的第 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
都没关系,现在 k = 2
我们先分析下状态转移方程:
1 2
| 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]);
|
发现没有可以化简的地方:
按照之前的代码,我们可能想当然这样写代码(错误的):
1 2 3 4 5 6 7 8 9 10 11 12 13
| int k = 2; int[][][] dp = new int[n][k + 1][2]; for (int i = 0; i < n; i++) { if (i - 1 == -1) { dp[i][k][0] = 0; dp[i][k][1] = -prices[i]; continue; } dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]); dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]); } return dp[n - 1][k][0];
|
为什么错误?我这不是照着状态转移方程写的吗?
还记得前面总结的「穷举框架」吗?就是说我们必须穷举所有状态。其实我们之前的解法,都在穷举所有状态,只是之前的题目中k
都被化简掉了。
但当k = 2
时,由于没有消掉k
的影响,所以必须要对k
进行穷举:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class Solution { public: int maxProfit(vector<int>& prices) { int n = prices.size(); int max_k = 2; vector<vector<vector<int> > > dp(n, vector<vector<int> > (max_k + 1, vector<int>(2, 0)));
for(int i = 0; i < n; i++){ for(int k = max_k; k >= 1; k--){ if(i - 1 == -1){ dp[i][k][0] = 0; dp[i][k][1] = -prices[i]; continue; } 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]); } } return dp[n - 1][max_k][0]; } };
|
状态压缩:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Solution { public: int maxProfit(vector<int>& prices) { int dp_i10 = 0, dp_i11 = INT_MIN; int dp_i20 = 0, dp_i21 = INT_MIN; for (int price : prices) { dp_i20 = max(dp_i20, dp_i21 + price); dp_i21 = max(dp_i21, dp_i10 - price); dp_i10 = max(dp_i10, dp_i11 + price); dp_i11 = max(dp_i11, -price); } return dp_i20; } };
|
给定一个整数数组 prices ,它的第 i 个元素 prices[i]
是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例:
输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4)
的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
思路:
k = nay interger
有了上一题k = 2
的铺垫,这题应该和上一题的第一个解法没啥区别。但是出现了一个超内存的错误,原来是传入的k
值会非常大,dp
数组太大了。现在想想,交易次数k
最多有多大呢?
一次交易由买入和卖出构成,至少需要两天。所以说有效的限制k
应该不超过n/2
,如果超过,就没有约束作用了,相当于k = +infinity
。这种情况是之前解决过的。
所以代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| class Solution { public: int maxProfit(int k, vector<int>& prices) { int n = prices.size(); if(n <= 0){ return 0; } if (k > n / 2) { return maxProfit2(prices); } vector<vector<vector<int> > > dp(n, vector<vector<int> > (k + 1, vector<int>(2, 0)));
for (int i = 0; i < n; i++) { dp[i][0][1] = INT_MIN; dp[i][0][0] = 0; }
for(int i = 0; i < n; i++){ for(int p = k; p >= 1; p--){ if(i - 1 == -1){ dp[i][p][0] = 0; dp[i][p][1] = -prices[i]; continue; } dp[i][p][0] = max(dp[i - 1][p][0], dp[i - 1][p][1] + prices[i]); dp[i][p][1] = max(dp[i - 1][p][1], dp[i - 1][p - 1][0] - prices[i]); } } return dp[n - 1][k][0]; } int maxProfit2(vector<int>& prices) { int dp_i10 = 0, dp_i11 = INT_MIN; int dp_i20 = 0, dp_i21 = INT_MIN; for (int price : prices) { dp_i20 = max(dp_i20, dp_i21 + price); dp_i21 = max(dp_i21, dp_i10 - price); dp_i10 = max(dp_i10, dp_i11 + price); dp_i11 = max(dp_i11, -price); } return dp_i20; } };
|