LeetCode 188:买卖股票的最佳时机 IV

LeetCode 188:买卖股票的最佳时机 IV

1. 题目核心

给定股票每天的价格 prices[i] 和整数 k,最多进行 k 笔完整交易,每笔交易必须是:

text 复制代码
买入 → 卖出

并且必须卖掉手里的股票之后才能再次买入。注意题目说的是最多 k 次 ,不是必须交易 k 次,所以可以交易 0、1、2...k 次。


2. 思路一:枚举所有买卖方案

最直观的方法是枚举:

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

例如:

text 复制代码
prices = [3,2,6,5,0,3]
k = 2

可以枚举出:

text 复制代码
2 买 → 6 卖,利润 4
0 买 → 3 卖,利润 3
总利润 = 7

这种方法的问题是组合数量非常多。第一笔交易确定以后还要继续枚举第二笔、第三笔,因此会出现大量重复计算,不适合直接实现。


3. 思路二:最直观的动态规划

3.1 DP 状态

定义:

text 复制代码
dp[t][i]

表示:

在第 0~i 天中,最多完成 t 笔交易时能够获得的最大利润。

例如:

text 复制代码
dp[2][5]

表示:

前 6 天里最多完成 2 笔交易能够获得的最大利润。

3.2 到第 i 天时只有两种情况

对于:

text 复制代码
dp[t][i]

第一种情况:今天不卖股票。那么最大利润就是昨天已经得到的:

text 复制代码
dp[t][i - 1]

第二种情况:今天卖股票 。假设第 j 天买入,第 i 天卖出,这一笔交易的利润为:

text 复制代码
prices[i] - prices[j]

但是在第 j 天买入之前,还可以已经完成最多 t-1 笔交易,所以总利润为:

text 复制代码
前面 t-1 笔交易的最大利润
+
当前这一笔交易的利润

即:

text 复制代码
dp[t-1][j-1] + prices[i] - prices[j]

所以完整公式为:

text 复制代码
dp[t][i]
=
max(
    dp[t][i-1],
    dp[t-1][j-1] + prices[i] - prices[j]
)

由于买入日 j 不确定,因此实际上还需要把前面所有可能的 j 都尝试一遍:

text 复制代码
dp[t][i]
=
max(
    dp[t][i-1],
    max(
        dp[t-1][j-1] + prices[i] - prices[j]
    )
)

3.3 max 到底在比较什么

它并不抽象,本质就是:

text 复制代码
以前已经找到的最好方案
vs
今天卖掉产生的新方案

谁利润高,就保留谁。

例如:

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

到价格 6 时:

text 复制代码
2 买 → 6 卖 = 4

所以最大利润变成 4。

第二天价格为 5:

text 复制代码
以前最好利润 = 4
今天卖 = 5 - 2 = 3

所以:

text 复制代码
max(4,3) = 4

说明今天不卖更好。

3.4 这个 DP 的问题

i 要遍历所有天,t 要遍历所有交易次数,还需要枚举买入日 j:

text 复制代码
时间复杂度:O(k × n²)

可以继续优化。


4. 最优思路:buy / sell 状态动态规划

把每一笔交易拆成两个状态:

text 复制代码
第1次买 → 第1次卖 → 第2次买 → 第2次卖 → ...

对于第 t 笔交易定义:

text 复制代码
buy[t]  = 第 t 次买入以后,目前能够保留的最好收益状态
sell[t] = 第 t 次卖出以后,目前能够获得的最大利润

例如 k = 2:

text 复制代码
buy[1] → sell[1] → buy[2] → sell[2]

5. 两个核心公式

5.1 买入公式

text 复制代码
buy[t] = max(
    buy[t],
    sell[t-1] - price
)

含义:

text 复制代码
方案1:今天不买
       → 保留以前的 buy[t]

方案2:今天买
       → 前 t-1 笔交易已经赚的钱 - 今天买股票花的钱

所以:

text 复制代码
buy[t] = max(以前买的最好状态, 今天买的状态)

例如第二次买:

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

表示第一笔已经赚了 4,现在价格为 0:

text 复制代码
buy[2] = 4 - 0 = 4

也就是:

text 复制代码
第一笔交易赚4
→ 0元买入第二次股票
→ 当前整体收益状态仍然为4

5.2 卖出公式

text 复制代码
sell[t] = max(
    sell[t],
    buy[t] + price
)

含义:

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

方案2:今天卖
       → 第 t 次买入后的状态 + 今天卖股票得到的钱

所以:

text 复制代码
sell[t] = max(以前卖出的最大利润, 今天卖出的利润)

6. 两条公式如何衔接

整个过程可以直接画成:

text 复制代码
sell[t-1]
    ↓
前 t-1 笔交易已经获得的利润
    ↓
减去今天股价
    ↓
buy[t]
    ↓
以后加上某一天的股价卖出
    ↓
sell[t]

所以核心只有:

text 复制代码
buy[t]  = max(buy[t], sell[t-1] - price)
sell[t] = max(sell[t], buy[t] + price)

其中所有 max 都是在比较:

text 复制代码
以前最好的结果
vs
今天进行操作得到的新结果

7. 示例完整推导

text 复制代码
prices = [3,2,6,5,0,3]
k = 2

状态:

text 复制代码
buy1  sell1  buy2  sell2

每天更新后的结果:

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

关键位置是价格 6:

text 复制代码
buy1 = -2
sell1 = -2 + 6 = 4

表示:

text 复制代码
2 买 → 6 卖
第一笔赚 4

后来价格变成 0:

text 复制代码
buy2 = sell1 - 0
     = 4

表示:

text 复制代码
第一笔已经赚 4
然后 0 元进行第二次买入

最后价格为 3:

text 复制代码
sell2 = buy2 + 3
      = 4 + 3
      = 7

最终:

text 复制代码
2 → 6,赚4
0 → 3,赚3
总利润 = 7

8. 为什么不用一定交易 k 次

题目要求:

text 复制代码
最多 k 次

而不是:

text 复制代码
必须 k 次

例如:

text 复制代码
prices = [10,8,6,4,2]

无论 k 多大,最优方案都是:

text 复制代码
一次都不交易
利润 = 0

如果最后一天才是最高价,也完全没问题。例如:

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

buy[1] 会一直保留最好的买入状态:

text 复制代码
2 元买入

到最后一天:

text 复制代码
sell[1] = -2 + 100 = 98

因此 DP 并不会提前强制卖出。


9. C++ 最优解

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

        for (int price : prices) {
            for (int t = k; t >= 1; --t) {
                sell[t] = max(sell[t], buy[t] + price);
                buy[t] = max(buy[t], sell[t - 1] - price);
            }
        }

        return sell[k];
    }
};

代码对应关系

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

表示:

text 复制代码
今天不卖
vs
今天进行第 t 次卖出
buy[t] = max(buy[t], sell[t - 1] - price);

表示:

text 复制代码
今天不买
vs
前 t-1 笔交易完成以后,今天进行第 t 次买入

交易次数从后往前:

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

主要是为了让 buy[t] 使用的 sell[t-1] 仍然是当前这一轮更新之前保存的状态。


10. Java 最优解

java 复制代码
class Solution {
    public int maxProfit(int k, int[] prices) {
        int[] buy = new int[k + 1];
        int[] sell = new int[k + 1];

        for (int t = 1; t <= k; t++) {
            buy[t] = -1000000000;
        }

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

        return sell[k];
    }
}

11. Java 语法解释

11.1 创建一维数组

java 复制代码
int[] buy = new int[k + 1];

表示创建一个长度为 k+1 的整数数组,对应 C++:

cpp 复制代码
vector<int> buy(k + 1);

Java 的 int 数组默认所有元素都是:

text 复制代码
0

11.2 增强 for 循环

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

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

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

也就是:

text 复制代码
第一天价格
第二天价格
第三天价格
...

11.3 Math.max

Java:

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

对应 C++:

cpp 复制代码
max(a, b)

所以:

java 复制代码
sell[t] = Math.max(sell[t], buy[t] + price);

就是:

text 复制代码
以前的最大利润
和
今天卖出的利润
取更大的一个

11.4 t--

java 复制代码
for (int t = k; t >= 1; t--)

表示:

text 复制代码
k, k-1, k-2, ..., 1

和 C++ 的写法基本完全一样。


12. Python 最优解

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

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

        return sell[k]

13. Python 语法解释

13.1 创建数组

python 复制代码
sell = [0] * (k + 1)

如果:

text 复制代码
k = 3

得到:

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

同理:

python 复制代码
buy = [-10**9] * (k + 1)

表示创建 k+1 个非常小的初始值。

13.2 10**9

Python:

python 复制代码
10**9

表示:

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

所以:

python 复制代码
-10**9

就是:

text 复制代码
-1000000000

13.3 遍历数组

python 复制代码
for price in prices:

表示每次取出一个股票价格,相当于 C++:

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

13.4 倒序遍历

python 复制代码
for t in range(k, 0, -1):

假设:

text 复制代码
k = 4

那么 t 依次为:

text 复制代码
4, 3, 2, 1

其中:

text 复制代码
range(开始值, 结束值, 步长)

所以:

python 复制代码
range(k, 0, -1)

表示从 k 开始,每次减 1,直到 1。

13.5 Python 的 max

python 复制代码
max(a, b)

直接返回两者中较大的一个,所以:

python 复制代码
buy[t] = max(buy[t], sell[t - 1] - price)

就是:

text 复制代码
以前的第 t 次买入状态
vs
今天进行第 t 次买入后的状态

14. 复杂度

最优动态规划中:

text 复制代码
每天遍历一次:n
每一天遍历 k 个交易状态:k

所以:

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

相比直观 DP 的:

text 复制代码
O(k × n²)

省掉了对所有历史买入日的重复枚举。


15. 核心记忆

状态

text 复制代码
buy[t]  = 第 t 次买入后的最好状态
sell[t] = 第 t 次卖出后的最大利润

买入

text 复制代码
buy[t] = max(
    以前的 buy[t],
    sell[t-1] - 今天价格
)

卖出

text 复制代码
sell[t] = max(
    以前的 sell[t],
    buy[t] + 今天价格
)

一句话理解

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

每个 max() 永远只是在比较:

text 复制代码
以前的最好方案
vs
今天操作产生的新方案

题目是最多交易 k 次,不是必须交易 k 次;DP 也不会提前固定哪天买卖,而是在每一天持续保留当前能达到的最佳状态。

相关推荐
启观川2 小时前
数据结构与算法 -第 3 章 常用算法-动态规划
数据结构·笔记·python·算法
码爸2 小时前
排序算法介绍
python·算法·排序算法
动词ing3 小时前
【题目练习】算法 组合总和||
算法
longlongzihan3 小时前
链表找环:Floyd 判圈算法(LeetCode 141 & 142 )
c++·算法·leetcode·链表
专业程序开发源3 小时前
flask家电故障预测系统92491-计算机课程设计/毕业设计
vscode·python·sql·算法·flask·课程设计
Logic1013 小时前
C语言/数据结构欧几里得算法题解:长方形切割最大正方形——贪心划分计数
c语言·数据结构·算法·贪心算法·时间复杂度·空间复杂度·欧几里得算法
yi0114 小时前
LeetCode 643. 子数组最大平均数 I|从暴力枚举到滑动窗口
人工智能·笔记·python·算法·leetcode·滑动窗口
极光通讯4 小时前
服务器内存来料检验(IQC)实操:批次追溯、外观判定与上机验证
java·服务器·算法
淡海水5 小时前
02-04-原理篇-GC Roots与对象图遍历
算法·unity·c#·图·gc·遍历·roots
ClickHouseDB5 小时前
ClickHouse 大规模 OpenTelemetry 数据接入架构的可靠性演进
算法