股票买卖类问题
该解法原创为英文版Leetcode用户fun4LeetCode
的题解:Most consistent ways of dealing with the series of stock problems。
本文主要作为总结该解法,并稍微改进一下通用公式,记录一下学习该解法时的想法。
题目描述
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
先买后卖,给定买卖限制条件,求最大收益。限制条件包括:买卖次数、冷冻期、手续费
Leecode相关题目:
1.买卖次数限制为1
2.买卖次数无限制
3.买卖次数限制为2
4.买卖次数限制为k
5.存在冻结期
6.含有手续费
模板解法
模板解法的核心是两个:一是动态规划,二是状态转换。
动态规划概念不细说,核心是利用前面的结果,根据转换方程,得到当前状态的结果。
状态在股票买卖类题目里面就两种:空仓、持股。这两个状态之间的切换:空仓->持股:买入;持股->空仓:卖出;持股->持股:维持;空仓->空仓:维持。同时,还要加上一个买卖次数限制。
根据状态切换的规则限制,可以得到以下的转换方程:
d
p
[
i
]
[
k
]
[
0
]
=
m
a
x
(
d
p
[
i
−
1
]
[
k
]
[
0
]
,
d
p
[
i
−
1
]
[
k
]
[
1
]
+
p
r
i
c
e
s
[
i
]
)
dp[i][k][0]=max(dp[i-1][k][0], dp[i-1][k][1]+prices[i])
dp[i][k][0]=max(dp[i−1][k][0],dp[i−1][k][1]+prices[i])
d
p
[
i
]
[
k
]
[
1
]
=
m
a
x
(
d
p
[
i
−
1
]
[
k
]
[
1
]
,
d
p
[
i
−
c
]
[
k
−
1
]
[
0
]
−
p
r
i
c
e
s
[
i
]
)
dp[i][k][1] = max(dp[i-1][k][1],dp[i-c][k-1][0]-prices[i])
dp[i][k][1]=max(dp[i−1][k][1],dp[i−c][k−1][0]−prices[i])
i
为
天
数
,
k
为
买
卖
次
数
,
0..1
为
空
仓
或
持
股
i为天数,k为买卖次数,0..1为空仓或持股
i为天数,k为买卖次数,0..1为空仓或持股
d
p
[
i
]
[
k
]
[
0..1
]
表
示
第
i
天
,
c
是
冻
结
时
间
,
买
卖
了
k
次
后
,
空
仓
或
持
股
状
态
下
的
最
大
收
益
dp[i][k][0..1]表示第i天,c是冻结时间,买卖了k次后,空仓或持股状态下的最大收益
dp[i][k][0..1]表示第i天,c是冻结时间,买卖了k次后,空仓或持股状态下的最大收益
解释一下上面的转换方程。当前空仓时最大收益是维持前一天空仓状态和前一天持股今天卖出收益之间的最大值。而当前持股时最大收益是维持前一天持股状态和经过c天冻结期的空仓后今天买入的最大值。
其他的很好理解,但是里面存在一个容易让人产生疑惑的状态切换,那就是经过c天冻结期的空仓后今天买入这一个切换。有的人(比如我)在初次理解时,会觉得存在“为什么不考虑 i − c + n i-c+n i−c+n这些天内的状态?要是我 c − n c-n c−n天前没有卖过股票,不在冻结期呢?”其实很容易理解:先不考虑卖出过股票的情况,如果 c − n c-n c−n天前没有卖出过,但又是空仓(因为最后一个状态位是 0 0 0),那么可以肯定的是,第 c − n c-n c−n天前和第 c − n − 1 c-n-1 c−n−1天前的值是相同的,以此类推,直到第 c c c天前。
那么再看一下边界状态。首先所有 k = = 0 k==0 k==0都为 0 0 0,因为买卖次数不够,禁止买卖,所以无收益。 d p [ − 1 ] [ k ] [ 0 ] = 0 dp[-1][k][0]=0 dp[−1][k][0]=0:初始空仓,无收益、 d p [ − 1 ] [ k ] [ 1 ] = − i n f i n i t y dp[-1][k][1]=-infinity dp[−1][k][1]=−infinity:初始未买入,无法卖出,所以负无穷。
在实际解题中,dp可以将天数的维度去掉,因为只会考虑前一天,前c天两种情况。在无买卖次数限制的情况下,空间复杂度可以为 O ( 1 ) O(1) O(1)
具体题解
1. 买卖次数限制为1
只需要考虑
k
=
=
1
k==1
k==1的情况,所以只记录dp_1_0
和dp_1_1
。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int dp_1_0 = 0, dp_1_1 = INT_MIN;
for (auto price : prices) {
dp_1_0 = max(dp_1_0, dp_1_1+price);
dp_1_1 = max(dp_1_1, -price); // 此处k-1 == 0,所以dp_0_0为0
}
return dp_1_0;
}
};
2. 买卖次数无限制
此时可以不用考虑买卖次数
k
k
k的影响,不会出现k==0
的情况,所以在计算dp_1
时,每次都使用上一次的dp_0
来计算。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int dp_0 = 0, dp_1 = INT_MIN;
for (auto price : prices) {
int temp_dp_0 = dp_0;
dp_0 = max(dp_0, dp_1+price);
dp_1 = max(dp_1, temp_dp_0-price); // 没有k的影响,不会出现k==0的情况
}
return dp_0;
}
};
然而此题可以再次简化,因为没有买卖次数限制,可以卖完立刻再买,所以只要出现后一天价格比前一天高,就可以前一天买后一天卖。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int res = 0;
for (int i = 0; i < prices.size()-1; i++) {
if (prices[i] < prices[i+1]) {
res += (prices[i+1]-prices[i]);
}
}
return res;
}
};
3. 买卖次数限制为2
此题买卖次数限制没有什么特殊性,需要依次计算。
class Solution {
public:
int maxProfit(vector<int>& prices) {
vector<vector<int>> dp(3, vector<int>(2, 0));
dp[0][1] = dp[1][1] = dp[2][1] = INT_MIN;
for (auto price : prices) {
for (int k = 1; k < 3; k++) {
dp[k][0] = max(dp[k][0], dp[k][1]+price);
dp[k][1] = max(dp[k][1], dp[k-1][0]-price);
}
}
return dp[2][0];
}
};
4. 买卖次数限制为k
此题更具有普遍性,
k
k
k可以为任意的数字,与第3题解法思路没什么区别。需要注意的是,当
k
k
k是一个很大的数字时,会出现内存错误,创建的dp
数组太大了。其实,当k > prices.size()/2
时,这个问题就已经退化为不限买卖次数的题目了。
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int l = prices.size();
if (k > l/2) {
int res = 0;
for (int i = 0; i < l-1; i++) {
if (prices[i+1] > prices[i]) {
res += (prices[i+1]-prices[i]);
}
}
return res;
}
vector<vector<int>> dp(k+1, vector<int>(2));
for (int i = 0; i < k+1; i++) {
dp[i][1] = INT_MIN;
}
for (auto price : prices) {
for (int i = 1; i < k+1; i++) {
dp[i][0] = max(dp[i][0], dp[i][1]+price);
dp[i][1] = max(dp[i][1], dp[i-1][0]-price);
}
}
return dp[k][0];
}
};
5. 存在冻结期
这题无买卖次数限制,但增加冻结期。需要增加一个记录冻结期前的空仓状态。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
int dp_0 = 0, dp_1 = INT_MIN, dp_pre_0 = 0;
for (int i = 0; i < n; i++) {
int temp_dp_0 = dp_0;
dp_0 = max(dp_0, dp_1+prices[i]);
dp_1 = max(dp_1, dp_pre_0 - prices[i]);
dp_pre_0 = temp_dp_0; // 记录冻结期前状态
}
return dp_0;
}
};
6. 含有手续费
只要在购买时,将手续费计算进去就行。
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int dp_0 = 0, dp_1 = INT_MIN;
for (auto price : prices) {
int temp_dp_0 = dp_0;
dp_0 = max(dp_0, dp_1+price);
dp_1 = max(dp_1, temp_dp_0-price-fee); // 计算上手续费
}
return dp_0;
}
};