我们以下面这道题来了解下股票问题这个框架。
具体可参考labuladong
每天都有三种「选择」:买入,卖出,无操作,我们用
buy,sell,rest表示三种选择。
但问题是,并不是每天都可以任意选择这三种选择,因为sell必须在buy之后,buy又必须在sell之后。那么rest操作之后还应该分两种状态:一种是buy之后的rest(持有了股票),一种是sell之后的rest(没有持有股票)。而且别忘了,我们还有交易次数k的限制,就是说你buy还只能在k > 0的前提下操作。
这个问题的「状态」有三个,第一个是天数,第二个是允许交易的最大次数,第三个是当前的持有状态(即之前说的rest的状态,我们不妨用
1 表示持有,0
表示没有持有)。然后我们用一个三维数组就可以装下这几种状态的全部组合:
| 12
 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)是如何转移而来的。根据这个图,我们来写一下状态转移方程:
| 12
 
 | 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。
| 12
 
 | 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。
| 12
 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
 解释:不允许交易的情况下,是不可能持有股票的。
 因为我们的算法要求一个最大值,所以初始值设为一个最小值,方便取最大值。
 
 | 
把上面的状态转移方程总结一下:
| 12
 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我们直接套模版,
| 12
 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])
 
 | 
写出如下代码:
| 12
 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 进行处理,可以这样给一个特化处理:
| 12
 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;
 }
 
 | 
所以代码如下:
| 12
 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)
| 12
 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是一样的,可以这样改写框架:
| 12
 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]);
 
 | 
所以可以写出:
| 12
 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)
| 12
 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之后要等一天才能继续交易。只要把这个特点融入上一题的状态转移方程即可:
| 12
 
 | 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]);
 
 | 
所以最终可以写成:
| 12
 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];
 }
 };
 
 | 
状态压缩:
| 12
 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
改写方程:
| 12
 
 | 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的整型溢出。
所以最终代码如下:
| 12
 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];
 }
 };
 
 | 
状态压缩如下
| 12
 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
我们先分析下状态转移方程:
| 12
 
 | 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]);
 
 | 
发现没有可以化简的地方:
按照之前的代码,我们可能想当然这样写代码(错误的):
| 12
 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进行穷举:
| 12
 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];
 }
 };
 
 | 
状态压缩:
| 12
 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。这种情况是之前解决过的。
所以代码如下:
| 12
 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;
 }
 };
 
 |