题目描述
给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格;非负整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
示例 1:
输入: prices = [1, 3, 2, 8, 4, 9], fee = 2
输出: 8
解释: 能够达到的最大利润:
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8
示例 2:
输入: prices = [1,3,7,5,10,3], fee = 3
输出: 6
提示:
- 1 <= prices.length <= 5 * 10^4
- 0 <= prices[i], fee < 5 * 10^4
问题分析
这是一个典型的股票交易问题,与之前的股票问题相比,这道题的特殊之处在于每次交易都需要支付一定的手续费。我们需要考虑手续费对交易决策的影响,并设计算法以最大化总利润。
主要约束条件:
- 可以进行任意次数的交易(买入和卖出)
- 每次交易都需要支付固定的手续费
- 必须先买入才能卖出,且卖出后才能再次买入(同一时刻最多持有一股股票)
解题思路
动态规划方法
这类问题最适合使用动态规划来解决。我们可以定义两个状态来表示每一天结束时的情况:
- 持有股票状态:表示当天结束时手上持有股票的最大利润
- 不持有股票状态:表示当天结束时手上没有股票的最大利润
状态定义
- hold[i]:表示第 i 天结束时,持有一支股票的最大利润
- cash[i]:表示第 i 天结束时,没有持有股票的最大利润
状态转移方程
对于每一天,我们有以下决策:
- 对于持有股票状态:
- 要么是前一天就持有股票,今天继续持有:hold[i-1]
- 要么是前一天没有股票,今天买入股票:cash[i-1] - prices[i]
hold[i] = max(hold[i-1], cash[i-1] - prices[i])
- 对于不持有股票状态:
- 要么是前一天就没有股票,今天继续不持有:cash[i-1]
- 要么是前一天持有股票,今天卖出(需要减去手续费):hold[i-1] + prices[i] - fee
cash[i] = max(cash[i-1], hold[i-1] + prices[i] - fee)
初始条件
- hold[0] = -prices[0]:第一天买入股票,利润为负的股票价格
- cash[0] = 0:第一天不操作,利润为0
最终答案
最后一天不持有股票的状态就是最大利润,即 cash[n-1]。
空间优化
由于每一天的状态只依赖于前一天的状态,我们可以使用两个变量代替数组,将空间复杂度从 O(n) 优化到 O(1)。
算法执行过程
以示例1为例:prices = [1, 3, 2, 8, 4, 9], fee = 2
初始状态:
- hold = -1(买入第一天的股票)
- cash = 0(不操作)
第2天(价格=3):
- prevCash = 0
- cash = max(0, -1 + 3 - 2) = max(0, 0) = 0
- hold = max(-1, 0 - 3) = max(-1, -3) = -1
第3天(价格=2):
- prevCash = 0
- cash = max(0, -1 + 2 - 2) = max(0, -1) = 0
- hold = max(-1, 0 - 2) = max(-1, -2) = -1
第4天(价格=8):
- prevCash = 0
- cash = max(0, -1 + 8 - 2) = max(0, 5) = 5
- hold = max(-1, 0 - 8) = max(-1, -8) = -1
第5天(价格=4):
- prevCash = 5
- cash = max(5, -1 + 4 - 2) = max(5, 1) = 5
- hold = max(-1, 5 - 4) = max(-1, 1) = 1
第6天(价格=9):
- prevCash = 5
- cash = max(5, 1 + 9 - 2) = max(5, 8) = 8
- hold = max(1, 5 - 9) = max(1, -4) = 1
最终结果:cash = 8
实例分析
让我们详细分析一下示例1的最优交易策略:
- 第1天:价格为1,买入股票,支出1,当前资金为-1
- 第4天:价格为8,卖出股票,获得8,支付手续费2,当前资金为5
- 第5天:价格为4,买入股票,支出4,当前资金为1
- 第6天:价格为9,卖出股票,获得9,支付手续费2,最终资金为8
这与我们的算法得出的最大利润8一致。
详细代码实现
Java 实现
class Solution {
public int maxProfit(int[] prices, int fee) {
// 边界检查
if (prices == null || prices.length <= 1) {
return 0;
}
int n = prices.length;
// 定义状态变量
// hold: 持有股票的最大利润
// cash: 不持有股票的最大利润
int hold = -prices[0];
int cash = 0;
// 遍历每一天
for (int i = 1; i < n; i++) {
// 临时保存前一天的cash值,因为更新hold时需要用到
int prevCash = cash;
// 更新cash(不持有股票的最大利润)
// 1. 前一天不持有股票,今天继续不持有
// 2. 前一天持有股票,今天卖出(减去手续费)
cash = Math.max(cash, hold + prices[i] - fee);
// 更新hold(持有股票的最大利润)
// 1. 前一天持有股票,今天继续持有
// 2. 前一天不持有股票,今天买入
hold = Math.max(hold, prevCash - prices[i]);
}
// 最后一天不持有股票的状态即为最大利润
return cash;
}
}
C# 实现
public class Solution {
public int MaxProfit(int[] prices, int fee) {
// 边界检查
if (prices == null || prices.Length <= 1) {
return 0;
}
int n = prices.Length;
// 定义状态变量
// hold: 持有股票的最大利润
// cash: 不持有股票的最大利润
int hold = -prices[0];
int cash = 0;
// 遍历每一天,计算最大利润
for (int i = 1; i < n; i++) {
// 保存当前状态,因为更新hold时会用到原来的cash值
int prevCash = cash;
// 不持有股票的最大利润
cash = Math.Max(cash, hold + prices[i] - fee);
// 持有股票的最大利润
hold = Math.Max(hold, prevCash - prices[i]);
}
// 最后一天不持有股票的状态即为最大利润
return cash;
}
}
复杂度分析
- 时间复杂度:O(n),其中n是价格数组的长度,我们只需要遍历一次数组。
- 空间复杂度:O(1),只使用了常数额外空间。