买卖股票问题

我们以下面这道题来了解下股票问题这个框架。

具体可参考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 为交易数的上限,01 代表是否持有股票。
此问题共 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])

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我们直接套模版,

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 = 0i - 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][0]
// = max(dp[-1][0], dp[-1][1] + prices[i])
// = max(0, -infinity + prices[i]) = 0

dp[i][1] = -prices[i];
// 根据状态转移方程可得:
// dp[i][1]
// = max(dp[-1][1], dp[-1][0] - prices[i])
// = max(-infinity, 0 - prices[i])
// = -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;
}
};

122. 买卖股票的最佳时机 II

给定一个数组 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,那么我们可以认为kk-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]);

//我们可以看到数组中 k 已经不会发生改变,也就是说不需要再记录 k
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;
}
};

309. 最佳买卖股票时机含冷冻期

给定一个整数数组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;//代表dp[i - 2][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;
}
};

714. 买卖股票的最佳时机含手续费

给定一个整数数组 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;
}
};

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都没关系,现在 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) {
// 处理 base case
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;
}
//k表示最大交易次数
dp[i][k][0] = max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i]);//2,我昨天持有股票,且截至昨天最大交易次数限制为 k;但是今天我 sell 了,所以我今天没有持有股票了,最大交易次数限制依然为 k。
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) {
// base case
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;
}
};

188. 买卖股票的最佳时机 IV

给定一个整数数组 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) {
// 复用之前交易次数 k 没有限制的情况
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) {
// base case
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;
}
};