给定一个整数数组prices
,其中第 prices[i]
表示第 i
天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
- 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: prices = [1,2,3,0,2] 输出: 3 解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
示例 2:
输入: prices = [1] 输出: 0
提示:
1 <= prices.length <= 5000
0 <= prices[i] <= 1000
问题分析
这个问题是"买卖股票的最佳时机"系列的变种,新增了一个重要的约束:卖出股票后的第二天不能买入(冷冻期)。
相比于前面的股票问题,我们需要考虑以下几点:
- 可以多次交易,但同一时间只能持有一股
- 卖出股票后的第二天不能买入
- 要求最大利润
这个约束会影响我们的决策,使得问题更加复杂。
解题思路
我们可以使用动态规划来解决这个问题。关键是要定义好状态和状态转移方程。
定义状态
考虑到冷冻期的存在,我们可以将每天结束时的状态分为三种:
- 持有股票状态 (hold):当天结束时持有一支股票
- 冷冻期状态 (cooldown):当天卖出了股票,下一天处于冷冻期
- 自由状态 (free):当天结束时没有持有股票,且不处于冷冻期
定义三个数组来记录这三种状态下的最大利润:
- hold[i]:第i天结束时,持有股票状态下的最大利润
- cooldown[i]:第i天结束时,处于冷冻期状态下的最大利润
- free[i]:第i天结束时,自由状态下的最大利润
状态转移
每天的状态会根据前一天的状态更新,规则如下:
- 持有股票 (hold[i]):
- 前一天就持有股票,今天什么都不做:hold[i-1]。
- 前一天不持有股票且不在冷冻期,今天买入:free[i-1] - prices[i]。
- 取两者的最大值:hold[i] = max(hold[i-1], free[i-1] - prices[i])。
- 不持有股票且处于冷冻期 (cooldown[i]):
- 前一天持有股票,今天卖出:hold[i-1] + prices[i]。
- 只有这一种情况:cooldown[i] = hold[i-1] + prices[i]。
- 不持有股票且不处于冷冻期 (free[i]):
- 前一天就不持有股票且不在冷冻期,今天不动:free[i-1]。
- 前一天处于冷冻期,今天冷冻期结束:cooldown[i-1]。
- 取两者的最大值:free[i] = max(free[i-1], cooldown[i-1])。
状态转移方程如下:
- hold[i] = max(hold[i-1], free[i-1] - prices[i])
- 今天继续持有昨天的股票,或者昨天是自由状态今天买入
- cooldown[i] = hold[i-1] + prices[i]
- 今天卖出股票,所以昨天必须是持有状态
- free[i] = max(free[i-1], cooldown[i-1])
- 今天继续是自由状态,或者从冷冻期转为自由状态
初始化
- 第一天 (i = 0):
- hold[0] = -prices[0]:买入股票,利润为负的股价。
- cooldown[0] = 0:第一天不可能卖出。
- free[0] = 0:不买不卖,利润为 0。
最终答案
最终的最大利润是 max(cooldown[n-1], free[n-1]),因为最后一天要么是卖出股票,要么是自由状态,都比持有股票的利润更高。
详细执行过程图解
以示例 [1,2,3,0,2] 为例,让我们详细跟踪算法的执行过程:
初始状态(第1天):
- 价格 = 1
- hold = -1(买入股票)
- cooldown = 0(不可能卖出)
- free = 0(不操作)
第2天:
- 价格 = 2
- hold = max(-1, 0-2) = -1(维持昨天买入的状态)
- cooldown = -1+2 = 1(今天卖出)
- free = max(0, 0) = 0(维持自由状态)
第3天:
- 价格 = 3
- hold = max(-1, 0-3) = -1(维持买入状态)
- cooldown = -1+3 = 2(今天卖出)
- free = max(0, 1) = 1(从冷冻期转为自由状态)
第4天:
- 价格 = 0
- hold = max(-1, 1-0) = 1(今天买入股票)
- cooldown = -1+0 = -1(今天卖出,但不如继续持有)
- free = max(1, 2) = 2(从冷冻期转为自由状态)
第5天:
- 价格 = 2
- hold = max(1, 2-2) = 1(维持买入状态)
- cooldown = 1+2 = 3(今天卖出)
- free = max(2, -1) = 2(维持自由状态)
最终结果 = max(cooldown, free) = max(3, 2) = 3
股票交易时间线图解
价格
^
3 | *
|
2 | * *
|
1 |*
|
0 +---------*---> 时间
1 2 3 4 5
交易策略:
第1天: 买入 (价格=1)
第2天: 卖出 (价格=2, 利润=1)
第3天: 冷冻期
第4天: 买入 (价格=0)
第5天: 卖出 (价格=2, 利润=2)
总利润: 1+2=3
代码实现
Java 实现
class Solution {
public int maxProfit(int[] prices) {
// 边界条件检查
if (prices == null || prices.length <= 1) {
return 0;
}
int n = prices.length;
// 初始化三个状态数组
int[] hold = new int[n]; // 持有股票
int[] cooldown = new int[n]; // 冷冻期(当天卖出)
int[] free = new int[n]; // 自由状态(不持有股票且不在冷冻期)
// 设置初始状态
hold[0] = -prices[0]; // 第一天买入
cooldown[0] = 0; // 第一天不可能卖出(因为之前没买入)
free[0] = 0; // 第一天不操作
// 动态规划过程
for (int i = 1; i < n; i++) {
// 第i天是持有股票状态,可能是之前买的,也可能是今天买的
hold[i] = Math.max(hold[i-1], free[i-1] - prices[i]);
// 第i天是冷冻期状态,必须是今天卖出了股票
cooldown[i] = hold[i-1] + prices[i];
// 第i天是自由状态,可能是之前就是自由状态,或者从冷冻期转来
free[i] = Math.max(free[i-1], cooldown[i-1]);
}
// 最大利润是最后一天不持有股票的情况,取冷冻期和自由状态的较大值
return Math.max(cooldown[n-1], free[n-1]);
}
}
C# 实现
public class Solution {
public int MaxProfit(int[] prices) {
// 边界条件检查
if (prices == null || prices.Length <= 1) {
return 0;
}
int n = prices.Length;
// 初始化三个状态数组
int[] hold = new int[n]; // 持有股票
int[] cooldown = new int[n]; // 冷冻期(当天卖出)
int[] free = new int[n]; // 自由状态(不持有股票且不在冷冻期)
// 设置初始状态
hold[0] = -prices[0]; // 第一天买入
cooldown[0] = 0; // 第一天不可能卖出
free[0] = 0; // 第一天不操作
// 动态规划过程
for (int i = 1; i < n; i++) {
// 第i天是持有股票状态,可能是之前买的,也可能是今天买的
hold[i] = Math.Max(hold[i-1], free[i-1] - prices[i]);
// 第i天是冷冻期状态,必须是今天卖出了股票
cooldown[i] = hold[i-1] + prices[i];
// 第i天是自由状态,可能是之前就是自由状态,或者从冷冻期转来
free[i] = Math.Max(free[i-1], cooldown[i-1]);
}
// 最大利润
return Math.Max(cooldown[n-1], free[n-1]);
}
}
空间优化
我们可以注意到,在每一步的计算中,我们只需要前一天的状态。因此,可以将空间复杂度从O(n)优化到O(1):
Java 空间优化版本
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length <= 1) {
return 0;
}
int hold = -prices[0]; // 持有股票状态
int cooldown = 0; // 冷冻期状态
int free = 0; // 自由状态
for (int i = 1; i < prices.length; i++) {
int prevHold = hold;
int prevCooldown = cooldown;
int prevFree = free;
// 更新三个状态
hold = Math.max(prevHold, prevFree - prices[i]);
cooldown = prevHold + prices[i];
free = Math.max(prevFree, prevCooldown);
}
return Math.max(cooldown, free);
}
}
C# 空间优化版本
public class Solution {
public int MaxProfit(int[] prices) {
if (prices == null || prices.Length <= 1) {
return 0;
}
int hold = -prices[0]; // 持有股票状态
int cooldown = 0; // 冷冻期状态
int free = 0; // 自由状态
for (int i = 1; i < prices.Length; i++) {
int prevHold = hold;
int prevCooldown = cooldown;
int prevFree = free;
// 更新三个状态
hold = Math.Max(prevHold, prevFree - prices[i]);
cooldown = prevHold + prices[i];
free = Math.Max(prevFree, prevCooldown);
}
return Math.Max(cooldown, free);
}
}
复杂度分析
- 时间复杂度:O(n),其中 n 是价格数组的长度。我们只需要遍历一次数组。
- 空间复杂度:
- 原始版本:O(n),需要三个长度为n的数组来存储状态
- 优化版本:O(1),只需要常数级别的额外空间