309. 买卖股票的最佳时机含冷冻期
问题描述
给定一个数组 prices
,其中 prices[i]
表示股票第 i
天的价格。设计算法计算最大利润,交易规则如下:
- 可进行多次交易
- 卖出股票后无法在第二天买入(冷冻期1天)
- 买入前必须卖出之前的股票
示例:
输入: [1,2,3,0,2]
输出: 3
解释:
第1天买入(1),第2天卖出(2),利润=1 → 进入冷冻期
第4天买入(0),第5天卖出(2),利润=2
总利润=1+2=3
算法思路
动态规划(DP 数组):
- 状态定义:
dp[i][0]
: 第i天结束时,持有股票的最大利润
dp[i][1]
: 第i天结束时,不持有股票且处于冷冻期(即当天卖出了股票)的最大利润
dp[i][2]
: 第i天结束时,不持有股票且不处于冷冻期(即当天没有操作,且没有冷冻期限制)的最大利润
注意:冷冻期表示卖出的后一天不能买入。因此,如果第i
天卖出了股票,那么第i+1
天不能买入。 - 状态转移:
dp[i][0](第i天持有股票)
:- 可能在第i-1天就持有股票,即dp[i-1][0]
- 或者在第i天买入股票。买入股票的条件是:第i-1天不能是冷冻期(即第i-1天没有卖出),所以只能从第i-1天的状态2(不持有且不处于冷冻期)转移过来,即dp[i-1][2] - prices[i]
因此:dp[i][0] = max(dp[i-1][0], dp[i-1][2] - prices[i])
dp[i][1](第i天卖出股票,所以第i天结束处于冷冻期)
:- 只能在第i天卖出股票,这意味着第i-1天必须持有股票,然后加上今天的价格。
因此:dp[i][1] = dp[i-1][0] + prices[i]
- 只能在第i天卖出股票,这意味着第i-1天必须持有股票,然后加上今天的价格。
dp[i][2](第i天不持有股票,且不处于冷冻期)
:- 说明第i天没有操作,且第i-1天也没有卖出(因为如果有卖出,第i天就是冷冻期,但这里是不处于冷冻期)。
- 所以第i-1天可能是状态1(冷冻期)或者状态2(非冷冻期)。但是注意,如果第i-1天是冷冻期,那么第i天就会解冻,变成非冷冻期;如果第i-1天是非冷冻期,那么第i天继续非冷冻期。
因此:dp[i][2] = max(dp[i-1][1], dp[i-1][2])
- 初始化:
第0天(i=0):
dp[0][0] =-prices[0]
// 买入股票
dp[0][1] =0
// 不可能在第0天卖出,可以认为卖出0(如果买入又卖出,则利润0)
dp[0][2] =0
// 第0天没有操作,不持有股票,且不是冷冻期 - 最终结果:
最后一天,不可能持有股票,所以取最后一天的不持有股票状态的最大值,即max(dp[n-1][1], dp[n-1][2])
动态规划(状态分离):
- 状态定义:
hold
:持有股票的最大利润sold
:当天卖出股票(进入冷冻期)的最大利润cooldown
:不持有股票且非冷冻期的最大利润
- 状态转移:
- 持有股票:
- 保持前日持有状态:
hold
- 今日买入(需从非冷冻期转移):
cooldown - prices[i]
- 保持前日持有状态:
- 卖出股票:
- 卖出前日持有的股票:
hold + prices[i]
- 卖出前日持有的股票:
- 冷冻期/非活跃:
- 取前日冷冻期和活跃状态的最大值:
max(sold, cooldown)
- 取前日冷冻期和活跃状态的最大值:
- 持有股票:
- 初始化:
- 第0天持有:
-prices[0]
- 第0天卖出:
0
- 第0天冷冻期:
0
- 第0天持有:
- 结果:
max(sold, cooldown)
(最终不持有股票)
代码实现
方法一:动态规划(状态变量)
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length <= 1) return 0;
// 初始化状态变量
int hold = -prices[0]; // 持有股票
int sold = 0; // 当天卖出(进入冷冻期)
int cooldown = 0; // 不持有股票且非冷冻期(可操作状态)
for (int i = 1; i < prices.length; i++) {
// 保存前一日状态(避免覆盖)
int prevHold = hold;
int prevSold = sold;
// 状态转移:
// 持有股票:max(保持持有, 今日买入)
// 买入必须从前日非冷冻期转移(cooldown)
hold = Math.max(prevHold, cooldown - prices[i]);
// 卖出股票:只能卖出前日持有的股票
sold = prevHold + prices[i];
// 冷冻期/非活跃:max(前日冷冻期, 前日活跃状态)
cooldown = Math.max(prevSold, cooldown);
}
// 最终结果取卖出状态和冷冻期状态的最大值
return Math.max(sold, cooldown);
}
}
方法二:动态规划(DP 数组)
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length <= 1) return 0;
int n = prices.length;
// dp[i][0]: 持有股票
// dp[i][1]: 当天卖出(进入冷冻期)
// dp[i][2]: 不持有股票且非冷冻期
int[][] dp = new int[n][3];
// 初始化第0天
dp[0][0] = -prices[0]; // 持有股票
dp[0][1] = 0; // 卖出状态(无操作)
dp[0][2] = 0; // 冷冻期/非活跃状态
for (int i = 1; i < n; i++) {
// 持有股票:max(保持持有, 今日买入)
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][2] - prices[i]);
// 卖出股票:卖出前日持有的股票
dp[i][1] = dp[i-1][0] + prices[i];
// 冷冻期/非活跃:max(前日冷冻期, 前日非活跃)
dp[i][2] = Math.max(dp[i-1][1], dp[i-1][2]);
}
return Math.max(dp[n-1][1], dp[n-1][2]);
}
}
算法分析
- 时间复杂度:O(n),遍历一次价格数组
- 空间复杂度:
- 状态变量:O(1)
- DP数组:O(n)(实际为3n)
算法过程
输入:prices = [1,2,3,0,2]
状态变量计算过程:
初始化:hold=-1, sold=0, cooldown=0
Day1(2):
hold = max(-1, 0-2) = -1 // 保持持有
sold = -1 + 2 = 1 // 卖出获利1
cooldown = max(0,0)=0 // 非活跃
Day2(3):
hold = max(-1, 0-3) = -1 // 保持持有(冷冻期无法买入)
sold = -1 + 3 = 2 // 卖出获利2
cooldown = max(1,0)=1 // 解冻昨日卖出
Day3(0):
hold = max(-1, 1-0)=1 // 买入(使用解冻后资金)
sold = -1 + 0 = -1 // 无意义(实际取cooldown转移)
cooldown = max(2,1)=2 // 保持非活跃
Day4(2):
hold = max(1, 2-2)=1 // 保持持有
sold = 1 + 2 = 3 // 卖出获利2(总利润3)
cooldown = max(-1,2)=2 // 非活跃
结果:max(3,2)=3
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1: 标准示例
int[] prices1 = {1,2,3,0,2};
System.out.println("Test 1: " + solution.maxProfit(prices1)); // 3
// 测试用例2: 价格持续上升
int[] prices2 = {1,2,3,4,5};
System.out.println("Test 2: " + solution.maxProfit(prices2)); // 4
// 测试用例3: 价格持续下降
int[] prices3 = {5,4,3,2,1};
System.out.println("Test 3: " + solution.maxProfit(prices3)); // 0
// 测试用例4: 波动价格
int[] prices4 = {2,1,4};
System.out.println("Test 4: " + solution.maxProfit(prices4)); // 3
// 测试用例5: 空数组
int[] prices5 = {};
System.out.println("Test 5: " + solution.maxProfit(prices5)); // 0
// 测试用例6: 两天可交易
int[] prices6 = {1,2};
System.out.println("Test 6: " + solution.maxProfit(prices6)); // 1
}
关键点
- 状态分离:
- 区分持有、卖出(冷冻期)、非活跃(可操作)三种状态
- 冷冻期后自动转为非活跃状态
- 状态转移核心:
- 买入操作必须从非活跃状态转移
- 卖出后强制进入冷冻期
- 初始化规则:
- 第0天持有股票的成本为
-prices[0]
- 第0天无卖出操作,冷冻期状态为0
- 第0天持有股票的成本为
- 空间优化:
- 用三个变量代替DP数组
- 按顺序更新避免状态覆盖
常见问题
- 为什么买入要从
cooldown
转移?
冷冻期结束后(非活跃状态)才能买入,确保遵守卖出后隔日才能买入的规则。 - 如何理解
cooldown
状态?
表示不持有股票且可立即操作的状态(非冷冻期)。 - 为何最后返回
max(sold, cooldown)
?
最终必须卖出股票,且可能处于冷冻期或非活跃状态。 - 如何处理连续下跌行情?
持有状态利润为负,卖出状态不会更新,最终返回0。