股票投资系列
总结了3道相关题目:
只有一次交易
直观上看,要使得收益最大,在买入时尽量地以低价购入,然后等股票价格最高时卖出即可。注意买入操作需要在卖出操作之前进行。
因此,从左到右依次遍历,维护一个最小值,表示当前时刻之前的股票的最低价格,然后再判断当天卖出是否能得到最大收益即可。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int mini = prices[0], res = 0;
for(int i = 1; i < prices.size(); i++) {
mini = min(prices[i], mini);
res = max(res, prices[i] - mini);
}
return res;
}
};
可以多次交易
在任意时刻,可以随意地卖出和买入,只需保证在买入之前先卖出就行(也就是假设只持有一支股票)。
注意到,如果当天卖出后立刻在当天又买入,其实总收益没有产生变化。比如:
[7, 1, 5, 3, 6, 4]
在第2天买入,在第4天卖出,赚了3 - 1 = 2
;
在第2天买入,然后在第三天卖出后立刻买入,然后又在第4天卖出,赚了3 - 5 + 5 - 1 = 2
根据这个性质,可以把所有连续的区间分解成长度为1的区间。问题转换成:给定n - 1
个长度为1的区间,选取哪些区间能够让总收益最大?
单个区间的收益很容易计算,右端点$-$左端点就是收益。
因此,总收益最大只需要看一下这些区间中,哪些区间的收益为正就可以了。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int res = 0;
for(int i = 0; i + 1 < prices.size(); i++) {
res += max(0, prices[i + 1] - prices[i]);
}
return res;
}
};
只能交易2次
求的是2段连续区间,可以考虑利用前后缀分解的做法。
前后缀分解是以第2段区间的左端点作为分割点,把问题分解成左右两端。
举个例子:
[3,3,5,0,0,3,1,4]
以1
作为分割点,问题被分解成:
[3,3,5,0,0,3]
交易一次的最大收益[1, 4]
交易一次的最大收益
那其实就是转换为2个问题一了。
所以,只需要枚举这个分割点,求全部情况的最大值即可。
按照这个思路,枚举分割点需要$O(n)$的时间复杂度,然后求左右两边的最大收益也是需要$O(n)$的复杂度,总的时间是$O(n^2)$的。能不能优化呢?
优化——预处理左区间
在问题一中,我们维护了一个最小值min,不妨设从1~i
区间交易1次的最大收益为f[i]
,则f[i]
可以由2种情况转移(递推)而来。
- 当前卖出产生的收益大于最大值,那就调整为在当前时刻卖出,
f[i] = prices[i] - min
; - 当前卖出产生的收益值小于最大值, 那就不卖,
f[i] = f[i - 1]
。
于是可以$O(n)$时间预处理出任意时刻的左边区间的最大收益。
优化——预处理右区间
借鉴前面的思路,我们要预处理出i~n
区间的最大收益。可以把问题一逆转一下,从后往前遍历(因为问题一求解的是从起点到i
的最大收益,而这里正向做的话,起点是变化的,不好处理)。不妨设从i~n
区间交易的最大收益为r[i]
,维护一个当前时刻右边的最大值,则r[i]
也可以从2种情况转移而来:
- 当前买入产生的收益大于最大值,那就调整为在当前时刻买入,
r[i] = max -prices[i]
; - 当前卖入产生的收益值小于最大值, 那就不买,
f[i] = f[i + 1]
。
于是可以$O(n)$时间预处理出任意时刻的右边区间的最大收益。
最后再遍历一下分割点即可。
总的时间复杂度是: $O(n) + O(n) + O(n) = O(n)$
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
vector<int> f(n + 1);
for(int i = 1, mini = INT_MAX; i <= n; i++) {
f[i] = max(f[i - 1], prices[i - 1] - mini);
mini = min(mini, prices[i - 1]);
}
int res = 0;
for(int i = n, maxi = 0; i; i--) {
res = max(res, maxi - prices[i - 1] + f[i - 1]);
maxi = max(maxi, prices[i - 1]);
}
return res;
}
};
只能交易k次
股票系列究极版。
如果$k \ge \lfloor \frac{n}{2} \rfloor$,那么等价于第二题(无限次操作)。因为一次交易需要买入和卖出2次操作,而一天只能进行一次操作,所以最多也就进行$\frac{n}{2} \rfloor$次交易。
对于$k < \lfloor \frac{n}{2} \rfloor$的情况,考虑用状态机DP来解决问题。
首先因为需要保证不能同时参与多笔交易,所以某一天只能有2种状态:手里没有股票、手里持有1支股票。
接下来考虑状态之间是如何转移的,不妨令这两种状态为$f$和$g$。
- $f \rightarrow g$: 当买入股票时,状态从没有股票转移到有1支股票,此时收益应该减少$w_i$(表示第$i$天股票的价格)
- $g \rightarrow f$: 当卖出股票时,状态从有1支股票转移到没有股票,此时收益应该增加$w_i$
- $f \rightarrow f$: 当手里没有股票时,当天不买入
- $g \rightarrow g$: 当手里有1支股票时,当天不卖出
于是构建出了一个状态机,接下来要在这个状态机上求解买卖股票的最大收益。
将问题转化为适用状态机模型的表达:
已知能走n条边,且最多能转k圈,求所有方案所得收益的最大值
回到DP的集合表示和集合计算。如图所示:
时间复杂度:$O(n^2)$
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int n = prices.size();
if(k >= n / 2) {
int res = 0;
for(int i = 0; i + 1 < n; i++) {
if(prices[i] < prices[i + 1])
res += prices[i + 1] - prices[i];
}
return res;
}
vector<vector<int>> f(n + 1, vector<int>(k + 1, -1e8));
auto g = f;
f[0][0] = 0;
int res = 0;
for(int i = 1; i <= n; i++) {
for(int j = 0; j <= k; j++) {
f[i][j] = max(f[i - 1][j], g[i - 1][j] + prices[i - 1]);
g[i][j] = g[i - 1][j];
if(j) g[i][j] = max(g[i][j], f[i - 1][j - 1] - prices[i - 1]);
res = max(res, f[i][j]);
}
}
return res;
}
};
含冷冻期
同样是利用状态机DP解决, 一共有3种状态,冷冻期,持有期,卖出期。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
vector<vector<int>> f(n + 1, vector<int>(3, 0));
f[0][0] = 0, f[0][1] = -prices[0];
for (int i = 1; i < n; i++) {
f[i][0] = max(f[i - 1][2], f[i - 1][0]);
f[i][1] = max(f[i - 1][0] - prices[i], f[i - 1][1]);
f[i][2] = f[i - 1][1] + prices[i];
}
return max(f[n - 1][0], f[n - 1][2]);
}
};
- Post link: https://scnujackychen.github.io/2021/03/14/LC-121/
- Copyright Notice: All articles in this blog are licensed under unless otherwise stated.
若没有本文 Issue,您可以使用 Comment 模版新建。
GitHub IssuesGitHub Discussions