题目链接 : 股票市场交易优化策略
题目分析
本题要求在规定规则下计算股票交易的最大利润。具体规则如下:
- 可以多次买卖股票,但在买入新的股票前必须先卖出手中的股票。
- 每次卖出股票后存在一天的冷冻期,冷冻期内不能购买股票。
需要我们设计一个算法,在给定的股票价格数组 stocks
下,计算能够获得的最大利润。
问题抽象
在这类问题中,核心在于如何在不违反规则的前提下,设计最优的买入和卖出策略。结合规则,问题可以分解为三个状态:
- 持有股票状态(hold) :表示当前手上持有股票。
- 未持有股票且非冷冻期状态(unfreeze) :表示手上没有股票,且可以随时购买。
- 冷冻期状态(freeze) :表示刚卖出股票,尚处于冷冻期,无法买入。
通过动态规划可以有效地解决该问题。
动态规划分析
动态规划是一种通过子问题求解全局问题的技术,适用于具有重叠子问题和最优子结构性质的问题。
状态定义
令 dp[i][0]
表示第 i
天未持有股票且不在冷冻期的最大利润; 令 dp[i][1]
表示第 i
天持有股票的最大利润。
注意:第
i
天是否处于冷冻期,可以由状态转移关系隐式表示。
状态转移方程
-
未持有股票状态:
-
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + stocks[i-1])
-
即当天未持有股票的最大利润为:
- 保持未持有股票状态(
dp[i-1][0]
)。 - 卖出手中股票获得收益(
dp[i-1][1] + stocks[i-1]
)。
- 保持未持有股票状态(
-
-
持有股票状态:
-
dp[i][1] = max(dp[i-1][1], dp[i-2][0] - stocks[i-1])
-
即当天持有股票的最大利润为:
- 保持持有股票状态(
dp[i-1][1]
)。 - 从冷冻期前一天的未持有状态买入股票(
dp[i-2][0] - stocks[i-1]
)。
- 保持持有股票状态(
-
边界条件
- 第一天未持有股票且非冷冻期:
dp[0][0] = 0
。 - 第一天持有股票:
dp[1][1] = -stocks[0]
。 - 冷冻期隐含在状态转移中,无需单独表示。
目标
我们需要求解的是 dp[n][0]
,即最后一天未持有股票的最大利润。
代码实现
以下是动态规划方法的实现,使用二维数组存储状态:
#include <bits/stdc++.h>
using namespace std;
int solution(std::vector<int> stocks) {
int n = stocks.size();
if (n == 0) return 0; // 无股票可交易
vector<vector<int>> dp(n + 1, vector<int>(2, -2e9)); // 1持有 , 0未持有
dp[0][0] = 0;
dp[1][0] = 0;
dp[1][1] = -stocks[0];
for (int i = 2; i <= n; i++) {
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + stocks[i - 1]);
dp[i][1] = max(dp[i - 1][1], dp[i - 2][0] - stocks[i - 1]);
}
return dp[n][0];
}
int main() {
std::cout << (solution({1, 2}) == 1) << std::endl;
std::cout << (solution({2, 1}) == 0) << std::endl;
std::cout << (solution({1, 2, 3, 0, 2}) == 3) << std::endl;
std::cout << (solution({2, 3, 4, 5, 6, 7}) == 5) << std::endl;
std::cout << (solution({1, 6, 2, 7, 13, 2, 8}) == 12) << std::endl;
return 0;
}
算法分析
时间复杂度
动态规划的时间复杂度为 O(n),因为我们仅需遍历一次股票数组,每次计算状态转移方程。
空间复杂度
代码中使用了一个二维数组存储状态,其空间复杂度为 O(n)。通过空间优化(仅记录当前天和前两天的状态),可将空间复杂度降低至 O(1)。
扩展
下面将介绍如何将空间复杂度降至O(1)
思路分析
在原始的动态规划实现中,我们使用了一个二维数组 dp[n+1][2]
,来存储第 iii 天的两个状态:未持有股票(0)和持有股票(1) 。但是,仔细观察状态转移方程:
- 计算
dp[i][0]
时,只需要用到dp[i-1][0]
和dp[i-1][1]
。 - 计算
dp[i][1]
时,只需要用到dp[i-1][1]
和dp[i-2][0]
。
这表明每次计算时,只需保留前两天的状态,而不需要整个数组。因此,可以通过变量替代数组的方式,将空间复杂度从 O(n) 降低到 O(1)。
状态变量替代
我们用以下变量分别表示特定状态:
prev_no_stock
: 表示前一天未持有股票的最大利润(对应dp[i-1][0]
)。prev_with_stock
: 表示前一天持有股票的最大利润(对应dp[i-1][1]
)。prev2_no_stock
: 表示前两天未持有股票的最大利润(对应dp[i-2][0]
)。
通过变量替代数组,每次将当前状态 curr_no_stock
和 curr_with_stock
更新为下一步的 prev_no_stock
和 prev_with_stock
,依次滚动推进。
核心代码如下
int solution(vector<int> stocks) {
int n = stocks.size();
if (n == 0) return 0; // 没有股票可交易
// 初始化状态
int prev2_no_stock = 0; // dp[i-2][0]
int prev_no_stock = 0; // dp[i-1][0]
int prev_with_stock = -stocks[0]; // dp[i-1][1]
for (int i = 1; i < n; i++) {
// 当前状态计算
int curr_no_stock = max(prev_no_stock, prev_with_stock + stocks[i]);
int curr_with_stock = max(prev_with_stock, prev2_no_stock - stocks[i]);
// 状态更新
prev2_no_stock = prev_no_stock;
prev_no_stock = curr_no_stock;
prev_with_stock = curr_with_stock;
}
return prev_no_stock; // 最后一天未持有股票的最大利润
}
总结
本题通过动态规划的方法,将股票交易的复杂约束转化为状态转移问题,设计合理的状态和转移关系是解题的关键。这其实是一类十分经典的动态规划题目 , 在力扣中也有类似的系列,可以为初学者们更好的学习动态规划, 所以我给大家把这个系列题目也贴到下面