LeetCode 123:买卖股票的最佳时机 III

LeetCode 123:买卖股票的最佳时机 III

1. 题目核心

给定每天的股票价格 prices[i],最多完成 2 笔交易,求最大利润。一笔完整交易是:

text 复制代码
买入 → 卖出

并且必须卖掉当前股票以后才能再次买入。注意是最多 2 笔 ,不是必须做 2 笔,因此可以做 0、1、2 笔交易。

例如:

text 复制代码
prices = [3,3,5,0,0,3,1,4]

最佳方案:

text 复制代码
0 买 → 3 卖:赚 3
1 买 → 4 卖:赚 3
总利润 = 6

2. 思路一:枚举两笔交易

最直接的想法是枚举:

text 复制代码
第一次:哪天买 → 哪天卖
第二次:哪天买 → 哪天卖

然后保证:

text 复制代码
第一次卖出时间 ≤ 第二次买入时间

计算所有合法方案的利润并取最大值。思路正确,但组合很多,会产生大量重复计算,因此不适合作为最优解。

3. 思路二:前后缀最大利润

因为最多只有两笔交易,也可以枚举"两笔交易的分界点"。例如在第 i 天切开:

text 复制代码
[ 左半部分 ] | [ 右半部分 ]
   最多1笔         最多1笔

分别计算:

text 复制代码
left[i]  = 第0~i天最多做1笔交易的最大利润
right[i] = 第i~最后一天最多做1笔交易的最大利润

然后:

text 复制代码
答案 = max(left[i] + right[i])

这种方法时间复杂度 O(n),但需要额外的前后缀数组,空间复杂度 O(n)

4. 最优思路:状态机动态规划

因为最多交易两次,所以真正需要记录的状态只有:

text 复制代码
buy[1]  = 第一次买入以后,持有股票时最好的收益状态
sell[1] = 第一次卖出以后,不持股时的最大利润

buy[2]  = 第二次买入以后,持有股票时最好的收益状态
sell[2] = 第二次卖出以后,不持股时的最大利润

整个过程就是:

text 复制代码
sell[0]
   ↓ 第1次买
 buy[1]
   ↓ 第1次卖
sell[1]
   ↓ 第2次买
 buy[2]
   ↓ 第2次卖
sell[2]

其中:

text 复制代码
sell[0] = 0

表示一笔交易都没做时,利润为 0

5. 最核心的两个公式

5.1 买入公式

cpp 复制代码
buy[j] = max(buy[j], sell[j - 1] - price);

这里是在比较两种方案:

text 复制代码
方案1:今天不买
       → 继续保持以前的 buy[j]

方案2:今天进行第 j 次买入
       → 前 j-1 笔交易赚的钱 - 今天买股票花的钱

因此:

text 复制代码
buy[j]
=
max(
    以前第j次买入后的最好状态,
    今天第j次买入后的状态
)

例如第一笔交易已经赚了 4

text 复制代码
sell[1] = 4

今天股票价格为 1,如果今天进行第二次买入:

text 复制代码
sell[1] - price
= 4 - 1
= 3

所以:

text 复制代码
buy[2] = 3

这个 3 不是第二笔交易赚了3元,而是:

text 复制代码
第一笔已经赚4元
- 第二次买股票花1元
= 当前整个交易过程的净收益状态3元

所以买入可以记成一句话:

前一笔卖完赚的钱 - 当前买入价格。

5.2 卖出公式

cpp 复制代码
sell[j] = max(sell[j], buy[j] + price);

同样比较两种方案:

text 复制代码
方案1:今天不卖
       → 保持以前的 sell[j]

方案2:今天进行第 j 次卖出
       → 当前 buy[j] 状态 + 今天卖股票得到的钱

例如:

text 复制代码
buy[2] = 3
今天价格 = 4

今天第二次卖出:

text 复制代码
3 + 4 = 7

于是:

text 复制代码
sell[2] = 7

卖出可以记成:

买入以后剩下的收益状态 + 当前卖出价格。

6. 两个公式怎么连起来

最重要的一张图:

text 复制代码
sell[j-1]
    ↓
减去当前股价
    ↓
 buy[j]
    ↓
加上当前股价
    ↓
sell[j]

因此:

cpp 复制代码
buy[j]  = max(buy[j], sell[j - 1] - price);
sell[j] = max(sell[j], buy[j] + price);

所有 max() 的含义完全一样:

以前保存的最好方案 vs 今天进行操作得到的新方案,留下更好的那个。

7. 为什么买入也要取 max

例如:

text 复制代码
prices = [3,2]

第一天价格 3

text 复制代码
buy[1] = -3

第二天价格变成 2

text 复制代码
以前买:-3
今天买:-2

显然:

text 复制代码
-2 > -3

所以:

cpp 复制代码
buy[1] = max(-3, -2) = -2;

它表示:

text 复制代码
原来3元买入不划算
现在改成2元买入更好

因此 buy 虽然经常是负数,仍然是越大越好

8. 为什么 DP 不需要提前决定哪天买卖

DP 不会真的在价格 6 时宣布"我已经卖了,以后不能反悔"。它只是在保存:

text 复制代码
截至今天,sell[1] 状态下能够达到的最好成绩是多少?

例如:

text 复制代码
prices = [2,6,5,100]

价格 6 时:

text 复制代码
sell[1] = 4

到了 5

text 复制代码
以前最好 = 4
今天卖 = 5 - 2 = 3
sell[1] = max(4,3) = 4

最后价格 100

text 复制代码
以前最好 = 4
今天卖 = 100 - 2 = 98
sell[1] = max(4,98) = 98

所以即使最后一天才是最高价,DP 也会自动把答案更新成最后一天卖。

9. 为什么不需要一定交易两次

题目说的是:

text 复制代码
最多交易2次

例如:

text 复制代码
prices = [7,6,4,3,1]

一直下跌,最优策略就是:

text 复制代码
什么都不做
利润 = 0

因此:

text 复制代码
sell[1] = 0
sell[2] = 0

最终答案也是 0。如果只做一笔交易最优,也不需要强行进行第二笔。

10. 示例完整推导

对于:

text 复制代码
prices = [3,3,5,0,0,3,1,4]

状态变化如下:

当前价格 buy1 sell1 buy2 sell2
3 -3 0 -3 0
3 -3 0 -3 0
5 -3 2 -3 2
0 0 2 2 2
0 0 2 2 2
3 0 3 2 5
1 0 3 2 5
4 0 4 2 6

最终:

text 复制代码
sell[2] = 6

对应一种最佳交易方案:

text 复制代码
0 → 3:赚3
1 → 4:赚3
总利润 = 6

11. C++ 动态规划代码

cpp 复制代码
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        vector<int> buy(3, -1000000000);
        vector<int> sell(3, 0);

        for (int i = 0; i < prices.size(); ++i) {
            for (int j = 2; j >= 1; --j) {
                buy[j] = max(buy[j], sell[j - 1] - prices[i]);
                sell[j] = max(sell[j], buy[j] + prices[i]);
            }
        }

        return sell[2];
    }
};

C++代码逐句对应

cpp 复制代码
vector<int> buy(3, -1000000000);

创建:

text 复制代码
buy[0], buy[1], buy[2]

实际使用 buy[1]、buy[2],初始设成非常小的数,因为一开始还不存在"已经买入"的状态。

cpp 复制代码
vector<int> sell(3, 0);

表示不交易时利润为 0,其中最重要的是:

text 复制代码
sell[0] = 0
for (int j = 2; j >= 1; --j)

依次处理:

text 复制代码
第2笔交易
第1笔交易

倒序可以保证:

cpp 复制代码
buy[j]

使用的:

cpp 复制代码
sell[j - 1]

还是之前保存的状态,而不是这一轮刚更新出来的值。

12. Java 动态规划代码

Java 使用的也是完全相同的动态规划算法。

java 复制代码
class Solution {
    public int maxProfit(int[] prices) {
        int[] buy = {-1000000000, -1000000000, -1000000000};
        int[] sell = new int[3];

        for (int price : prices) {
            for (int j = 2; j >= 1; j--) {
                buy[j] = Math.max(buy[j], sell[j - 1] - price);
                sell[j] = Math.max(sell[j], buy[j] + price);
            }
        }

        return sell[2];
    }
}

13. Java 语法解释

13.1 一维数组

java 复制代码
int[] buy = {-1000000000, -1000000000, -1000000000};

表示创建整数数组:

text 复制代码
buy[0]
buy[1]
buy[2]

对应 C++:

cpp 复制代码
vector<int> buy(3, -1000000000);

13.2 new int[3]

java 复制代码
int[] sell = new int[3];

创建长度为 3 的整数数组。Java 的 int 数组默认全部初始化为 0,因此得到:

text 复制代码
[0,0,0]

13.3 增强 for 循环

java 复制代码
for (int price : prices)

表示依次取出 prices 中的每一个元素,相当于 C++:

cpp 复制代码
for (int price : prices)

13.4 Math.max

Java:

java 复制代码
Math.max(a, b)

对应 C++:

cpp 复制代码
max(a, b)

例如:

java 复制代码
buy[j] = Math.max(buy[j], sell[j - 1] - price);

就是比较:

text 复制代码
以前买入的状态
vs
今天买入的状态

14. Python 动态规划代码

Python 同样使用完全相同的动态规划算法。

python 复制代码
class Solution:
    def maxProfit(self, prices):
        buy = [-10**9] * 3
        sell = [0] * 3

        for price in prices:
            for j in range(2, 0, -1):
                buy[j] = max(buy[j], sell[j - 1] - price)
                sell[j] = max(sell[j], buy[j] + price)

        return sell[2]

15. Python 语法解释

15.1 创建数组

python 复制代码
buy = [-10**9] * 3

10**9 表示:

text 复制代码
10的9次方 = 1000000000

因此:

python 复制代码
[-10**9] * 3

得到:

text 复制代码
[-1000000000, -1000000000, -1000000000]
sell = [0] * 3

得到:

text 复制代码
[0,0,0]

15.2 遍历数组

python 复制代码
for price in prices:

表示依次拿出每天的股票价格。

15.3 倒序循环

python 复制代码
for j in range(2, 0, -1):

range(开始, 结束, 步长),因此这里产生:

text 复制代码
2, 1

注意不会包含结束值 0

15.4 max

python 复制代码
max(a, b)

返回两个值中较大的一个,因此:

python 复制代码
sell[j] = max(sell[j], buy[j] + price)

表示:

text 复制代码
以前卖出的最大利润
vs
今天卖出的利润

16. 复杂度

最多只有两笔交易,每一天只更新 buy[1]、sell[1]、buy[2]、sell[2]

text 复制代码
时间复杂度:O(n)
空间复杂度:O(1)

虽然代码使用了两个长度为 3 的数组,但数组大小固定,不随 n 增长,所以空间复杂度仍然是 O(1)

17. 与 LeetCode 188 的关系

LeetCode 123:

text 复制代码
最多交易2次

所以:

cpp 复制代码
for (int j = 2; j >= 1; --j)

LeetCode 188:

text 复制代码
最多交易k次

直接改成:

cpp 复制代码
for (int j = k; j >= 1; --j)

两个核心公式完全不变:

cpp 复制代码
buy[j] = max(buy[j], sell[j - 1] - price);
sell[j] = max(sell[j], buy[j] + price);

18. 核心记忆

先记状态:

text 复制代码
buy[j]  = 第j次买入以后,持股状态下最好的总收益
sell[j] = 第j次卖出以后,不持股状态下的最大总利润

再记资金流:

text 复制代码
sell[j-1]
    ↓ 买入要花钱,所以减价格
 buy[j]
    ↓ 卖出收钱,所以加价格
sell[j]

对应两条公式:

cpp 复制代码
buy[j] = max(buy[j], sell[j - 1] - price);
sell[j] = max(sell[j], buy[j] + price);

最后只记一句:

买 = 前一次卖完的钱 - 当前价格;卖 = 当前买入状态 + 当前价格;每次都和以前的最好结果取 max

相关推荐
Zane19941 小时前
写对二分查找有多难?Java集合框架的作者也曾栽在一行mid计算上
算法
Lokey8681 小时前
transformer结构
算法
罗斯8391 小时前
EMBER恶意软件基准数据集
人工智能·算法·安全·网络安全
0+1111 小时前
算法 --二分查找
c++·算法·leetcode
Omics Pro2 小时前
新型条件传输模型!虚拟细胞扰动预测
数据库·人工智能·算法·机器学习·自然语言处理
黄金龙PLUS2 小时前
5个800比特大状态置换算法的设计与分析
算法·网络安全·密码学·哈希算法·同态加密
政企项目老覃2 小时前
大模型 Agent 自主任务编排:电商客服地址解析从 12% 失败率到 2.1% 的落地复盘
人工智能·程序人生·算法
kukubuzai3 小时前
双指针系列二--末尾篇(3道题)
c++·算法·leetcode
杜 硕3 小时前
单链表经典算法题
数据结构·算法