团灭股票问题.md 26.7 KB
Newer Older
L
labuladong 已提交
1 2
# 团灭 LeetCode 股票买卖问题

L
update  
labuladong 已提交
3
<!-- [团灭 LeetCode 股票买卖问题](https://mp.weixin.qq.com/s/4nqJMIyCKQD7IJ-HI6S3Vg) -->
4 5 6 7

<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
L
update  
labuladong 已提交
8
<a href="https://gitee.com/labuladong/upic/raw/master/2021_04_23/21_28_41.jpg"><img src="https://img.shields.io/badge/公众号-@labuladong-000000.svg?style=flat-square&logo=WeChat"></a>
9 10 11
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>

L
update  
labuladong 已提交
12
![](../pictures/souyisou1.png)
13

L
labuladong 已提交
14
**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
15 16 17

读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:

L
update  
labuladong 已提交
18
[121. 买卖股票的最佳时机(简单)](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/)
19

L
update  
labuladong 已提交
20
[122. 买卖股票的最佳时机 II(简单)](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/)
21

L
update  
labuladong 已提交
22
[123. 买卖股票的最佳时机 III(困难)](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/)
23

L
update  
labuladong 已提交
24
[188. 买卖股票的最佳时机 IV(困难)](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/)
25

L
update  
labuladong 已提交
26
[309. 最佳买卖股票时机含冷冻期(中等)](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/)
27

L
update  
labuladong 已提交
28
[714. 买卖股票的最佳时机含手续费(中等)](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/)
29 30 31

**-----------**

L
labuladong 已提交
32 33
很多读者抱怨 LeetCode 的股票系列问题奇技淫巧太多,如果面试真的遇到这类问题,基本不会想到那些巧妙的办法,怎么办?**所以本文拒绝奇技淫巧,而是稳扎稳打,只用一种通用方法解决所用问题,以不变应万变**

L
fixurl  
labuladong 已提交
34
这篇文章参考 [英文版高赞题解](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/discuss/108870/Most-consistent-ways-of-dealing-with-the-series-of-stock-problems) 的思路,用状态机的技巧来解决,可以全部提交通过。不要觉得这个名词高大上,文学词汇而已,实际上就是 DP table,看一眼就明白了。
L
labuladong 已提交
35 36 37 38 39 40

先随便抽出一道题,看看别人的解法:

```cpp
int maxProfit(vector<int>& prices) {
    if(prices.empty()) return 0;
L
update  
labuladong 已提交
41 42 43
    int s1 = -prices[0], s2 = INT_MIN, s3 = INT_MIN, s4 = INT_MIN;

    for(int i = 1; i < prices.size(); ++i) {            
L
labuladong 已提交
44
        s1 = max(s1, -prices[i]);
L
update  
labuladong 已提交
45 46 47
        s2 = max(s2, s1 + prices[i]);
        s3 = max(s3, s2 - prices[i]);
        s4 = max(s4, s3 + prices[i]);
L
labuladong 已提交
48
    }
L
update  
labuladong 已提交
49
    return max(0, s4);
L
labuladong 已提交
50 51 52 53 54 55 56 57 58 59 60
}
```

能看懂吧?会做了吗?不可能的,你看不懂,这才正常。就算你勉强看懂了,下一个问题你还是做不出来。为什么别人能写出这么诡异却又高效的解法呢?因为这类问题是有框架的,但是人家不会告诉你的,因为一旦告诉你,你五分钟就学会了,该算法题就不再神秘,变得不堪一击了。

本文就来告诉你这个框架,然后带着你一道一道秒杀。这篇文章用状态机的技巧来解决,可以全部提交通过。不要觉得这个名词高大上,文学词汇而已,实际上就是 DP table,看一眼就明白了。

这 6 道题目是有共性的,我就抽出来第 4 道题目,因为这道题是一个最泛化的形式,其他的问题都是这个形式的简化,看下题目:

![](../pictures/%E8%82%A1%E7%A5%A8%E9%97%AE%E9%A2%98/title.png)

L
update  
labuladong 已提交
61
第一题是只进行一次交易,相当于 `k = 1`;第二题是不限交易次数,相当于 `k = +infinity`(正无穷);第三题是只进行 2 次交易,相当于 `k = 2`;剩下两道也是不限次数,但是加了交易「冷冻期」和「手续费」的额外条件,其实就是第二题的变种,都很容易处理。
L
labuladong 已提交
62 63 64

如果你还不熟悉题目,可以去 LeetCode 查看这些题目的内容,本文为了节省篇幅,就不列举这些题目的具体内容了。下面言归正传,开始解题。

L
update  
labuladong 已提交
65
### 一、穷举框架
L
labuladong 已提交
66

L
update  
labuladong 已提交
67
首先,还是一样的思路:如何穷举?
L
labuladong 已提交
68

L
update  
labuladong 已提交
69
[动态规划核心套路](../动态规划系列/动态规划详解进阶.md) 说过,动态规划算法本质上就是穷举「状态」,然后在「选择」中选择最优解。
L
labuladong 已提交
70

L
update  
labuladong 已提交
71
那么对于这道题,我们具体到每一天,看看总共有几种可能的「状态」,再找出每个「状态」对应的「选择」。我们要穷举所有「状态」,穷举的目的是根据对应的「选择」更新状态。听起来抽象,你只要记住「状态」和「选择」两个词就行,下面实操一下就很容易明白了。
L
labuladong 已提交
72 73 74 75 76 77 78 79

```python
for 状态1 in 状态1的所有取值
    for 状态2 in 状态2的所有取值
        for ...
            dp[状态1][状态2][...] = 择优(选择1选择2...)
```

L
update  
labuladong 已提交
80 81 82 83 84
比如说这个问题,**每天都有三种「选择」**:买入、卖出、无操作,我们用 `buy`, `sell`, `rest` 表示这三种选择。

但问题是,并不是每天都可以任意选择这三种选择的,因为 `sell` 必须在 `buy` 之后,`buy` 必须在 `sell` 之后。那么 `rest` 操作还应该分两种状态,一种是 `buy` 之后的 `rest`(持有了股票),一种是 `sell` 之后的 `rest`(没有持有股票)。而且别忘了,我们还有交易次数 `k` 的限制,就是说你 `buy` 还只能在 `k > 0` 的前提下操作。

很复杂对吧,不要怕,我们现在的目的只是穷举,你有再多的状态,老夫要做的就是一把梭全部列举出来。
L
labuladong 已提交
85

L
update  
labuladong 已提交
86
**这个问题的「状态」有三个**,第一个是天数,第二个是允许交易的最大次数,第三个是当前的持有状态(即之前说的 `rest` 的状态,我们不妨用 1 表示持有,0 表示没有持有)。然后我们用一个三维数组就可以装下这几种状态的全部组合:
L
labuladong 已提交
87 88 89

```python
dp[i][k][0 or 1]
L
update  
labuladong 已提交
90 91
0 <= i <= n - 1, 1 <= k <= K
n 为天数 K 为交易数的上限0  1 代表是否持有股票
L
labuladong 已提交
92 93 94 95 96 97 98 99 100 101
此问题共 n × K × 2 种状态全部穷举就能搞定

for 0 <= i < n:
    for 1 <= k <= K:
        for s in {0, 1}:
            dp[i][k][s] = max(buy, sell, rest)
```

而且我们可以用自然语言描述出每一个状态的含义,比如说 `dp[3][2][1]` 的含义就是:今天是第三天,我现在手上持有着股票,至今最多进行 2 次交易。再比如 `dp[2][3][0]` 的含义:今天是第二天,我现在手上没有持有股票,至今最多进行 3 次交易。很容易理解,对吧?

L
update  
labuladong 已提交
102 103 104
我们想求的最终答案是 `dp[n - 1][K][0]`,即最后一天,最多允许 `K` 次交易,最多获得多少利润。

读者可能问为什么不是 `dp[n - 1][K][1]`?因为 `dp[n - 1][K][1]` 代表到最后一天手上还持有股票,`dp[n - 1][K][0]` 表示最后一天手上的股票已经卖出去了,很显然后者得到的利润一定大于前者。 
L
labuladong 已提交
105 106 107

记住如何解释「状态」,一旦你觉得哪里不好理解,把它翻译成自然语言就容易理解了。

L
update  
labuladong 已提交
108 109 110
### 二、状态转移框架

现在,我们完成了「状态」的穷举,我们开始思考每种「状态」有哪些「选择」,应该如何更新「状态」。
L
labuladong 已提交
111

L
update  
labuladong 已提交
112
只看「持有状态」,可以画个状态转移图:
L
labuladong 已提交
113 114 115 116 117

![](../pictures/%E8%82%A1%E7%A5%A8%E9%97%AE%E9%A2%98/1.png)

通过这个图可以很清楚地看到,每种状态(0 和 1)是如何转移而来的。根据这个图,我们来写一下状态转移方程:

L
update  
labuladong 已提交
118
```python
L
labuladong 已提交
119
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
L
update  
labuladong 已提交
120 121
              max( 今天选择 rest,        今天选择 sell       )
```
L
labuladong 已提交
122

L
update  
labuladong 已提交
123
解释:今天我没有持有股票,有两种可能,我从这两种可能中求最大利润:
L
labuladong 已提交
124

L
update  
labuladong 已提交
125
1、我昨天就没有持有,且截至昨天最大交易次数限制为 `k`;然后我今天选择 `rest`,所以我今天还是没有持有,最大交易次数限制依然为 `k`
L
labuladong 已提交
126

L
update  
labuladong 已提交
127 128 129 130 131
2、我昨天持有股票,且截至昨天最大交易次数限制为 `k`;但是今天我 `sell` 了,所以我今天没有持有股票了,最大交易次数限制依然为 `k`

```python
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
              max( 今天选择 rest,         今天选择 buy         )
L
labuladong 已提交
132 133
```

L
update  
labuladong 已提交
134
解释:今天我持有着股票,最大交易次数限制为 `k`,那么对于昨天来说,有两种可能,我从这两种可能中求最大利润:
L
labuladong 已提交
135

L
update  
labuladong 已提交
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
1、我昨天就持有着股票,且截至昨天最大交易次数限制为 `k`;然后今天选择 `rest`,所以我今天还持有着股票,最大交易次数限制依然为 `k`

2、我昨天本没有持有,且截至昨天最大交易次数限制为 `k - 1`;但今天我选择 `buy`,所以今天我就持有股票了,最大交易次数限制为 `k`

> 这里着重提醒一下,时刻牢记「状态」的定义,`k` 的定义并不是「已进行的交易次数」,而是「最大交易次数的上限限制」。如果确定今天进行一次交易,且要保证截至今天最大交易次数上限为 `k`,那么昨天的最大交易次数上限必须是 `k - 1`。

这个解释应该很清楚了,如果 `buy`,就要从利润中减去 `prices[i]`,如果 `sell`,就要给利润增加 `prices[i]`。今天的最大利润就是这两种可能选择中较大的那个。

注意 `k` 的限制,在选择 `buy` 的时候相当于开启了一次交易,那么对于昨天来说,交易次数的上限 `k` 应该减小 1。

> 修正:以前我以为在 `sell` 的时候给 `k` 减小 1 和在 `buy` 的时候给 `k` 减小 1 是等效的,但细心的读者向我提出质疑,经过深入思考我发现前者确实是错误的,因为交易是从 `buy` 开始,如果 `buy` 的选择不改变交易次数 `k` 的约束,会出现交易次数超出限制的的错误。

现在,我们已经完成了动态规划中最困难的一步:状态转移方程。**如果之前的内容你都可以理解,那么你已经可以秒杀所有问题了,只要套这个框架就行了**。不过还差最后一点点,就是定义 base case,即最简单的情况。 

```python
dp[-1][...][0] = 0
解释因为 i 是从 0 开始的所以 i = -1 意味着还没有开始这时候的利润当然是 0

dp[-1][...][1] = -infinity
解释还没开始的时候是不可能持有股票的
因为我们的算法要求一个最大值所以初始值设为一个最小值方便取最大值

dp[...][0][0] = 0
解释因为 k 是从 1 开始的所以 k = 0 意味着根本不允许交易这时候利润当然是 0

dp[...][0][1] = -infinity
解释不允许交易的情况下是不可能持有股票的
因为我们的算法要求一个最大值所以初始值设为一个最小值方便取最大值
L
labuladong 已提交
164 165 166 167
```

把上面的状态转移方程总结一下:

L
update  
labuladong 已提交
168
```python
L
labuladong 已提交
169
base case
L
update  
labuladong 已提交
170 171
dp[-1][...][0] = dp[...][0][0] = 0
dp[-1][...][1] = dp[...][0][1] = -infinity
L
labuladong 已提交
172 173 174 175 176 177 178 179

状态转移方程
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
```

读者可能会问,这个数组索引是 -1 怎么编程表示出来呢,负无穷怎么表示呢?这都是细节问题,有很多方法实现。现在完整的框架已经完成,下面开始具体化。

L
update  
labuladong 已提交
180
### 三、秒杀题目
L
labuladong 已提交
181 182 183 184 185

**第一题,k = 1**

直接套状态转移方程,根据 base case,可以做一些化简:

L
update  
labuladong 已提交
186
```python
L
labuladong 已提交
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
dp[i][1][1] = max(dp[i-1][1][1], dp[i-1][0][0] - prices[i]) 
            = max(dp[i-1][1][1], -prices[i])
解释k = 0  base case所以 dp[i-1][0][0] = 0

现在发现 k 都是 1不会改变 k 对状态转移已经没有影响了
可以进行进一步化简去掉所有 k
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], -prices[i])
```

直接写出代码:

```java
int n = prices.length;
int[][] dp = new int[n][2];
for (int i = 0; i < n; i++) {
    dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
    dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
}
return dp[n - 1][0];
```

L
update  
labuladong 已提交
210
显然 `i = 0``i - 1` 是不合法的索引,这是因为我们没有对 `i` 的 base case 进行处理,可以这样给一个特化处理:
L
labuladong 已提交
211 212

```java
L
update  
labuladong 已提交
213 214 215 216 217 218 219 220 221 222 223 224 225 226
if (i - 1 == -1) {
    dp[i][0] = 0;
    // 根据状态转移方程可得:
    //   dp[i][0] 
    // = max(dp[-1][0], dp[-1][1] + prices[i])
    // = max(0, -infinity + prices[i]) = 0

    dp[i][1] = -prices[i];
    // 根据状态转移方程可得:
    //   dp[i][1] 
    // = max(dp[-1][1], dp[-1][0] - prices[i])
    // = max(-infinity, 0 - prices[i]) 
    // = -prices[i]
    continue;
L
labuladong 已提交
227 228 229
}
```

L
update  
labuladong 已提交
230
第一题就解决了,但是这样处理 base case 很麻烦,而且注意一下状态转移方程,新状态只和相邻的一个状态有关,其实不用整个 `dp` 数组,只需要一个变量储存相邻的那个状态就足够了,这样可以把空间复杂度降到 O(1):
L
labuladong 已提交
231 232

```java
L
update  
labuladong 已提交
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
// 原始版本
int maxProfit_k_1(int[] prices) {
    int n = prices.length;
    int[][] dp = new int[n][2];
    for (int i = 0; i < n; i++) {
        if (i - 1 == -1) {
            // base case
            dp[i][0] = 0;
            dp[i][1] = -prices[i];
            continue;
        }
        dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
        dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
    }
    return dp[n - 1][0];
}

// 空间复杂度优化版本
L
labuladong 已提交
251 252 253 254 255 256 257 258 259 260 261 262 263 264
int maxProfit_k_1(int[] prices) {
    int n = prices.length;
    // base case: dp[-1][0] = 0, dp[-1][1] = -infinity
    int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
    for (int i = 0; i < n; i++) {
        // dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
        dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
        // dp[i][1] = max(dp[i-1][1], -prices[i])
        dp_i_1 = Math.max(dp_i_1, -prices[i]);
    }
    return dp_i_0;
}
```

L
update  
labuladong 已提交
265
两种方式都是一样的,不过这种编程方法简洁很多,但是如果没有前面状态转移方程的引导,是肯定看不懂的。后续的题目,你可以对比一下如何把 `dp` 数组的空间优化掉。
L
labuladong 已提交
266 267 268

**第二题,k = +infinity**

L
update  
labuladong 已提交
269
如果 `k` 为正无穷,那么就可以认为 `k``k - 1` 是一样的。可以这样改写框架:
L
labuladong 已提交
270 271 272 273 274 275 276 277 278 279 280 281 282 283

```python
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
            = max(dp[i-1][k][1], dp[i-1][k][0] - prices[i])

我们发现数组中的 k 已经不会改变了也就是说不需要记录 k 这个状态了
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
```

直接翻译成代码:

```java
L
update  
labuladong 已提交
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
// 原始版本
int maxProfit_k_inf(int[] prices) {
    int n = prices.length;
    int[][] dp = new int[n][2];
    for (int i = 0; i < n; i++) {
        if (i - 1 == -1) {
            // base case
            dp[i][0] = 0;
            dp[i][1] = -prices[i];
            continue;
        }
        dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
        dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]);
    }
    return dp[n - 1][0];
}

// 空间复杂度优化版本
L
labuladong 已提交
302 303 304 305 306 307 308 309 310 311 312 313 314 315
int maxProfit_k_inf(int[] prices) {
    int n = prices.length;
    int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
    for (int i = 0; i < n; i++) {
        int temp = dp_i_0;
        dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
        dp_i_1 = Math.max(dp_i_1, temp - prices[i]);
    }
    return dp_i_0;
}
```

**第三题,k = +infinity with cooldown**

L
update  
labuladong 已提交
316
每次 `sell` 之后要等一天才能继续交易。只要把这个特点融入上一题的状态转移方程即可:
L
labuladong 已提交
317

L
update  
labuladong 已提交
318
```python
L
labuladong 已提交
319 320 321 322 323 324 325 326
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i])
解释 i 天选择 buy 的时候要从 i-2 的状态转移而不是 i-1 
```

翻译成代码:

```java
L
update  
labuladong 已提交
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
// 原始版本
int maxProfit_with_cool(int[] prices) {
    int n = prices.length;
    int[][] dp = new int[n][2];
    for (int i = 0; i < n; i++) {
        if (i - 1 == -1) {
            // base case 1
            dp[i][0] = 0;
            dp[i][1] = -prices[i];
            continue;
        }
        if (i - 2 == -1) {
            // base case 2
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
            // i - 2 小于 0 时根据状态转移方程推出对应 base case
            dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
            //   dp[i][1] 
            // = max(dp[i-1][1], dp[-1][0] - prices[i])
            // = max(dp[i-1][1], 0 - prices[i])
            // = max(dp[i-1][1], -prices[i])
            continue;
        }
        dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
        dp[i][1] = Math.max(dp[i-1][1], dp[i-2][0] - prices[i]);
    }
    return dp[n - 1][0];
}

// 空间复杂度优化版本
L
labuladong 已提交
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
int maxProfit_with_cool(int[] prices) {
    int n = prices.length;
    int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
    int dp_pre_0 = 0; // 代表 dp[i-2][0]
    for (int i = 0; i < n; i++) {
        int temp = dp_i_0;
        dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
        dp_i_1 = Math.max(dp_i_1, dp_pre_0 - prices[i]);
        dp_pre_0 = temp;
    }
    return dp_i_0;
}
```

**第四题,k = +infinity with fee**

每次交易要支付手续费,只要把手续费从利润中减去即可。改写方程:

L
update  
labuladong 已提交
374
```python
L
labuladong 已提交
375 376 377 378 379 380
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee)
解释相当于买入股票的价格升高了
在第一个式子里减也是一样的相当于卖出股票的价格减小了
```

L
update  
labuladong 已提交
381 382 383
> 如果直接把 `fee` 放在第一个式子里减,会有测试用例无法通过,错误原因是整型溢出而不是思路问题。一种解决方案是把代码中的 `int` 类型都改成 `long` 类型,避免 `int` 的整型溢出。

直接翻译成代码,注意状态转移方程改变后 base case 也要做出对应改变:
L
labuladong 已提交
384 385

```java
L
update  
labuladong 已提交
386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408
// 原始版本
int maxProfit_with_fee(int[] prices, int fee) {
    int n = prices.length;
    int[][] dp = new int[n][2];
    for (int i = 0; i < n; i++) {
        if (i - 1 == -1) {
            // base case
            dp[i][0] = 0;
            dp[i][1] = -prices[i] - fee;
            //   dp[i][1]
            // = max(dp[i - 1][1], dp[i - 1][0] - prices[i] - fee)
            // = max(dp[-1][1], dp[-1][0] - prices[i] - fee)
            // = max(-inf, 0 - prices[i] - fee)
            // = -prices[i] - fee
            continue;
        }
        dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
        dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i] - fee);
    }
    return dp[n - 1][0];
}

// 空间复杂度优化版本
L
labuladong 已提交
409 410 411 412 413 414 415 416 417 418 419 420 421 422
int maxProfit_with_fee(int[] prices, int fee) {
    int n = prices.length;
    int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
    for (int i = 0; i < n; i++) {
        int temp = dp_i_0;
        dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
        dp_i_1 = Math.max(dp_i_1, temp - prices[i] - fee);
    }
    return dp_i_0;
}
```

**第五题,k = 2**

L
update  
labuladong 已提交
423
`k = 2` 和前面题目的情况稍微不同,因为上面的情况都和 `k` 的关系不太大。要么 `k` 是正无穷,状态转移和 `k` 没关系了;要么 `k = 1`,跟 `k = 0` 这个 base case 挨得近,最后也没有存在感。
L
labuladong 已提交
424

L
update  
labuladong 已提交
425
这道题 `k = 2` 和后面要讲的 `k` 是任意正整数的情况中,对 `k` 的处理就凸显出来了。我们直接写代码,边写边分析原因。
L
labuladong 已提交
426 427

```java
L
update  
labuladong 已提交
428
原始的状态转移方程没有可化简的地方
L
labuladong 已提交
429 430 431 432 433 434 435 436 437
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
```

按照之前的代码,我们可能想当然这样写代码(错误的):

```java
int k = 2;
int[][][] dp = new int[n][k + 1][2];
L
update  
labuladong 已提交
438 439 440 441 442 443 444
for (int i = 0; i < n; i++) {
    if (i - 1 == -1) {
        // 处理 base case
        dp[i][k][0] = 0;
        dp[i][k][1] = -prices[i];
        continue;
    }
L
labuladong 已提交
445 446 447 448 449 450 451 452
    dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
    dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
}
return dp[n - 1][k][0];
```

为什么错误?我这不是照着状态转移方程写的吗?

L
update  
labuladong 已提交
453
还记得前面总结的「穷举框架」吗?就是说我们必须穷举所有状态。其实我们之前的解法,都在穷举所有状态,只是之前的题目中 `k` 都被化简掉了。
L
labuladong 已提交
454

L
update  
labuladong 已提交
455
比如说第一题,`k = 1` 时的代码框架:
L
labuladong 已提交
456 457

```java
L
update  
labuladong 已提交
458 459
int n = prices.length;
int[][] dp = new int[n][2];
L
labuladong 已提交
460
for (int i = 0; i < n; i++) {
L
update  
labuladong 已提交
461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484
    dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
    dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
}
return dp[n - 1][0];
```

但当 `k = 2` 时,由于没有消掉 `k` 的影响,所以必须要对 `k` 进行穷举:

```java
// 原始版本
int maxProfit_k_2(int[] prices) {
    int max_k = 2, n = prices.length;
    int[][][] dp = new int[n][max_k + 1][2];
    for (int i = 0; i < n; i++) {
        for (int k = max_k; k >= 1; k--) {
            if (i - 1 == -1) {
                // 处理 base case
                dp[i][k][0] = 0;
                dp[i][k][1] = -prices[i];
                continue;
            }
            dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
            dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
        }
L
labuladong 已提交
485
    }
L
update  
labuladong 已提交
486 487
    // 穷举了 n × max_k × 2 个状态,正确。
    return dp[n - 1][max_k][0];
L
labuladong 已提交
488 489 490
}
```

L
update  
labuladong 已提交
491
> **PS:这里肯定会有读者疑惑,`k` 的 base case 是 0,按理说应该从 `k = 1, k++` 这样穷举状态 `k` 才对?而且如果你真的这样从小到大遍历 `k`,提交发现也是可以的**。
L
labuladong 已提交
492

L
update  
labuladong 已提交
493 494 495 496 497 498 499 500 501
这个疑问很正确,因为我们前文 [动态规划答疑篇](../动态规划系列/最优子结构.md) 有介绍 `dp` 数组的遍历顺序是怎么确定的,主要是根据 base case,以 base case 为起点,逐步向结果靠近。

但为什么我从大到小遍历 `k` 也可以正确提交呢?因为你注意看,`dp[i][k]` 不会依赖 `dp[i][k - 1]`,而是依赖 `dp[i - 1][k - 1]`,对于 `dp[i - 1][...]`,都是已经计算出来的。所以不管你是 `k = max_k, k--`,还是 `k = 1, k++`,都是可以得出正确答案的。

那为什么我使用 `k = max_k, k--` 的方式呢?因为这样符合语义。

你买股票,初始的「状态」是什么?应该是从第 0 天开始,而且还没有进行过买卖,所以最大交易次数限制 `k` 应该是 `max_k`;而随着「状态」的推移,你会进行交易,那么交易次数上限 `k` 应该不断减少,这样一想,`k = max_k, k--` 的方式是比较合乎实际场景的。

当然,这里 `k` 取值范围比较小,所以可以不用 for 循环,直接把 k = 1 和 2 的情况全部列举出来也可以:
L
labuladong 已提交
502 503

```java
L
update  
labuladong 已提交
504 505 506 507 508
// 状态转移方程:
// dp[i][2][0] = max(dp[i-1][2][0], dp[i-1][2][1] + prices[i])
// dp[i][2][1] = max(dp[i-1][2][1], dp[i-1][1][0] - prices[i])
// dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
// dp[i][1][1] = max(dp[i-1][1][1], -prices[i])
L
labuladong 已提交
509

L
update  
labuladong 已提交
510
// 空间复杂度优化版本
L
labuladong 已提交
511
int maxProfit_k_2(int[] prices) {
L
update  
labuladong 已提交
512
    // base case
L
labuladong 已提交
513 514 515 516 517 518 519 520 521 522 523 524
    int dp_i10 = 0, dp_i11 = Integer.MIN_VALUE;
    int dp_i20 = 0, dp_i21 = Integer.MIN_VALUE;
    for (int price : prices) {
        dp_i20 = Math.max(dp_i20, dp_i21 + price);
        dp_i21 = Math.max(dp_i21, dp_i10 - price);
        dp_i10 = Math.max(dp_i10, dp_i11 + price);
        dp_i11 = Math.max(dp_i11, -price);
    }
    return dp_i20;
}
```

L
update  
labuladong 已提交
525
有状态转移方程和含义明确的变量名指导,相信你很容易看懂。其实我们可以故弄玄虚,把上述四个变量换成 `a, b, c, d`。这样当别人看到你的代码时就会大惊失色,对你肃然起敬。
L
labuladong 已提交
526 527 528

**第六题,k = any integer**

L
update  
labuladong 已提交
529
有了上一题 `k = 2` 的铺垫,这题应该和上一题的第一个解法没啥区别。但是出现了一个超内存的错误,原来是传入的 `k` 值会非常大,`dp` 数组太大了。现在想想,交易次数 `k` 最多有多大呢?
L
labuladong 已提交
530

L
update  
labuladong 已提交
531
一次交易由买入和卖出构成,至少需要两天。所以说有效的限制 `k` 应该不超过 `n/2`,如果超过,就没有约束作用了,相当于 `k = +infinity`。这种情况是之前解决过的。
L
labuladong 已提交
532 533 534 535 536 537

直接把之前的代码重用:

```java
int maxProfit_k_any(int max_k, int[] prices) {
    int n = prices.length;
L
update  
labuladong 已提交
538 539 540 541 542
    if (n <= 0) {
        return 0;
    }
    if (max_k > n / 2) {
        // 交易次数 k 没有限制的情况
L
labuladong 已提交
543
        return maxProfit_k_inf(prices);
L
update  
labuladong 已提交
544
    }
L
labuladong 已提交
545

L
update  
labuladong 已提交
546 547 548
    // base case:
    // dp[-1][...][0] = dp[...][0][0] = 0
    // dp[-1][...][1] = dp[...][0][1] = -infinity
L
labuladong 已提交
549
    int[][][] dp = new int[n][max_k + 1][2];
L
update  
labuladong 已提交
550 551 552 553 554 555
    // k = 0 时的 base case
    for (int i = 0; i < n; i++) {
        dp[i][0][1] = Integer.MIN_VALUE;
        dp[i][0][0] = 0;
    }

L
labuladong 已提交
556 557
    for (int i = 0; i < n; i++) 
        for (int k = max_k; k >= 1; k--) {
L
update  
labuladong 已提交
558 559 560 561 562 563 564 565
            if (i - 1 == -1) {
                // 处理 i = -1 时的 base case
                dp[i][k][0] = 0;
                dp[i][k][1] = -prices[i];
                continue;
            }
            dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
            dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);     
L
labuladong 已提交
566 567 568 569 570 571 572 573 574 575 576
        }
    return dp[n - 1][max_k][0];
}
```

至此,6 道题目通过一个状态转移方程全部解决。

**四、最后总结**

本文给大家讲了如何通过状态转移的方法解决复杂的问题,用一个状态转移方程秒杀了 6 道股票买卖问题,现在想想,其实也不算难对吧?这已经属于动态规划问题中较困难的了。

L
update  
labuladong 已提交
577
关键就在于列举出所有可能的「状态」,然后想想怎么穷举更新这些「状态」。一般用一个多维 `dp` 数组储存这些状态,从 base case 开始向后推进,推进到最后的状态,就是我们想要的答案。想想这个过程,你是不是有点理解「动态规划」这个名词的意义了呢?
L
labuladong 已提交
578 579 580

具体到股票买卖问题,我们发现了三个状态,使用了一个三维数组,无非还是穷举 + 更新,不过我们可以说的高大上一点,这叫「三维 DP」,怕不怕?这个大实话一说,立刻显得你高人一等,名利双收有没有,所以给个在看/分享吧,鼓励一下我。

581
**_____________**
582

L
labuladong 已提交
583
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
L
labuladong 已提交
584

585
**本小抄即将出版,微信扫码关注公众号,后台回复「小抄」限时免费获取,回复「进群」可进刷题群一起刷题,带你搞定 LeetCode**
L
labuladong 已提交
586

587
<p align='center'>
L
labuladong 已提交
588
<img src="../pictures/qrcode.jpg" width=200 >
589
</p>