Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
即将拥有人鱼线的大哥
Fucking Algorithm
提交
c185c1ea
F
Fucking Algorithm
项目概览
即将拥有人鱼线的大哥
/
Fucking Algorithm
与 Fork 源项目一致
从无法访问的项目Fork
通知
1
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
DevOps
流水线
流水线任务
计划
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
F
Fucking Algorithm
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
DevOps
DevOps
流水线
流水线任务
计划
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
流水线任务
提交
Issue看板
前往新版Gitcode,体验更适合开发者的 AI 搜索 >>
未验证
提交
c185c1ea
编写于
12月 10, 2021
作者:
L
labuladong
提交者:
GitHub
12月 10, 2021
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
update
上级
441d69d2
变更
1
隐藏空白更改
内联
并排
Showing
1 changed file
with
276 addition
and
496 deletion
+276
-496
动态规划系列/团灭股票问题.md
动态规划系列/团灭股票问题.md
+276
-496
未找到文件。
动态规划系列/团灭股票问题.md
浏览文件 @
c185c1ea
# 团灭 LeetCode 股票买卖问题
<!-- [团灭 LeetCode 股票买卖问题](https://mp.weixin.qq.com/s/4nqJMIyCKQD7IJ-HI6S3Vg) -->
<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>
<a href="https://
i.loli.net/2020/10/10/MhRTyUKfXZOlQYN
.jpg"><img src="https://img.shields.io/badge/公众号-@labuladong-000000.svg?style=flat-square&logo=WeChat"></a>
<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>
<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>
![](
../pictures/souyisou.png
)
![](
../pictures/souyisou
1
.png
)
**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**
~
读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
[
买卖股票的最佳时机
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock
)
[
121. 买卖股票的最佳时机(简单)
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/
)
[
买卖股票的最佳时机 II
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/
)
[
122. 买卖股票的最佳时机 II(简单)
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/
)
[
买卖股票的最佳时机 III
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/
)
[
123. 买卖股票的最佳时机 III(困难)
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/
)
[
买卖股票的最佳时机 IV
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/
)
[
188. 买卖股票的最佳时机 IV(困难)
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/
)
[
最佳买卖股票时机含冷冻期
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/
)
[
309. 最佳买卖股票时机含冷冻期(中等)
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/
)
[
买卖股票的最佳时机含手续费
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/
)
[
714. 买卖股票的最佳时机含手续费(中等)
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/
)
**-----------**
...
...
@@ -37,15 +38,15 @@
```
cpp
int
maxProfit
(
vector
<
int
>&
prices
)
{
if
(
prices
.
empty
())
return
0
;
int
s1
=-
prices
[
0
],
s2
=
INT_MIN
,
s3
=
INT_MIN
,
s4
=
INT_MIN
;
for
(
int
i
=
1
;
i
<
prices
.
size
();
++
i
)
{
int
s1
=
-
prices
[
0
],
s2
=
INT_MIN
,
s3
=
INT_MIN
,
s4
=
INT_MIN
;
for
(
int
i
=
1
;
i
<
prices
.
size
();
++
i
)
{
s1
=
max
(
s1
,
-
prices
[
i
]);
s2
=
max
(
s2
,
s1
+
prices
[
i
]);
s3
=
max
(
s3
,
s2
-
prices
[
i
]);
s4
=
max
(
s4
,
s3
+
prices
[
i
]);
s2
=
max
(
s2
,
s1
+
prices
[
i
]);
s3
=
max
(
s3
,
s2
-
prices
[
i
]);
s4
=
max
(
s4
,
s3
+
prices
[
i
]);
}
return
max
(
0
,
s4
);
return
max
(
0
,
s4
);
}
```
...
...
@@ -57,17 +58,17 @@ int maxProfit(vector<int>& prices) {
![](
../pictures/%E8%82%A1%E7%A5%A8%E9%97%AE%E9%A2%98/title.png
)
第一题是只进行一次交易,相当于
k = 1;第二题是不限交易次数,相当于 k = +infinity(正无穷);第三题是只进行 2 次交易,相当于 k = 2
;剩下两道也是不限次数,但是加了交易「冷冻期」和「手续费」的额外条件,其实就是第二题的变种,都很容易处理。
第一题是只进行一次交易,相当于
`k = 1`
;第二题是不限交易次数,相当于
`k = +infinity`
(正无穷);第三题是只进行 2 次交易,相当于
`k = 2`
;剩下两道也是不限次数,但是加了交易「冷冻期」和「手续费」的额外条件,其实就是第二题的变种,都很容易处理。
如果你还不熟悉题目,可以去 LeetCode 查看这些题目的内容,本文为了节省篇幅,就不列举这些题目的具体内容了。下面言归正传,开始解题。
**一、穷举框架**
### 一、穷举框架
首先,还是一样的思路:如何穷举?
这里的穷举思路和上篇文章递归的思想不太一样。
首先,还是一样的思路:如何穷举?
递归其实是符合我们思考的逻辑的,一步步推进,遇到无法解决的就丢给递归,一不小心就做出来了,可读性还很好。缺点就是一旦出错,你也不容易找到错误出现的原因。比如上篇文章的递归解法,肯定还有计算冗余,但确实不容易找到
。
[
动态规划核心套路
](
../动态规划系列/动态规划详解进阶.md
)
说过,动态规划算法本质上就是穷举「状态」,然后在「选择」中选择最优解
。
而这里,我们不用递归思想进行穷举,而是利用「状态」进行穷举。
我们具体到每一天,看看总共有几种可能的「状态」,再找出每个「状态」对应的「选择」。我们要穷举所有「状态」,穷举的目的是根据对应的「选择」更新状态。听起来抽象,你只要记住「状态」和「选择」两个词就行,下面实操一下就很容易明白了。
那么对于这道题,
我们具体到每一天,看看总共有几种可能的「状态」,再找出每个「状态」对应的「选择」。我们要穷举所有「状态」,穷举的目的是根据对应的「选择」更新状态。听起来抽象,你只要记住「状态」和「选择」两个词就行,下面实操一下就很容易明白了。
```
python
for
状态1
in
状态1的所有取值
:
...
...
@@ -76,14 +77,18 @@ for 状态1 in 状态1的所有取值:
dp
[
状态1
][
状态2
][...]
=
择优
(
选择1
,
选择2
...)
```
比如说这个问题,
**每天都有三种「选择」**
:买入、卖出、无操作,我们用 buy, sell, rest 表示这三种选择。但问题是,并不是每天都可以任意选择这三种选择的,因为 sell 必须在 buy 之后,buy 必须在 sell 之后。那么 rest 操作还应该分两种状态,一种是 buy 之后的 rest(持有了股票),一种是 sell 之后的 rest(没有持有股票)。而且别忘了,我们还有交易次数 k 的限制,就是说你 buy 还只能在 k > 0 的前提下操作。
比如说这个问题,
**每天都有三种「选择」**
:买入、卖出、无操作,我们用
`buy`
,
`sell`
,
`rest`
表示这三种选择。
但问题是,并不是每天都可以任意选择这三种选择的,因为
`sell`
必须在
`buy`
之后,
`buy`
必须在
`sell`
之后。那么
`rest`
操作还应该分两种状态,一种是
`buy`
之后的
`rest`
(持有了股票),一种是
`sell`
之后的
`rest`
(没有持有股票)。而且别忘了,我们还有交易次数
`k`
的限制,就是说你
`buy`
还只能在
`k > 0`
的前提下操作。
很复杂对吧,不要怕,我们现在的目的只是穷举,你有再多的状态,老夫要做的就是一把梭全部列举出来。
很复杂对吧,不要怕,我们现在的目的只是穷举,你有再多的状态,老夫要做的就是一把梭全部列举出来。
**这个问题的「状态」有三个**
,第一个是天数,第二个是允许交易的最大次数,第三个是当前的持有状态(即之前说的 rest
的状态,我们不妨用 1 表示持有,0 表示没有持有)。然后我们用一个三维数组就可以装下这几种状态的全部组合:
**这个问题的「状态」有三个**
,第一个是天数,第二个是允许交易的最大次数,第三个是当前的持有状态(即之前说的
`rest`
的状态,我们不妨用 1 表示持有,0 表示没有持有)。然后我们用一个三维数组就可以装下这几种状态的全部组合:
```
python
dp
[
i
][
k
][
0
or
1
]
0
<=
i
<=
n
-
1
,
1
<=
k
<=
K
n
为天数
,
大
K
为
最多交易数
0
<=
i
<=
n
-
1
,
1
<=
k
<=
K
n
为天数
,
大
K
为
交易数的上限
,
0
和
1
代表是否持有股票
。
此问题共
n
×
K
×
2
种状态
,
全部穷举就能搞定
。
for
0
<=
i
<
n
:
...
...
@@ -94,54 +99,76 @@ for 0 <= i < n:
而且我们可以用自然语言描述出每一个状态的含义,比如说
`dp[3][2][1]`
的含义就是:今天是第三天,我现在手上持有着股票,至今最多进行 2 次交易。再比如
`dp[2][3][0]`
的含义:今天是第二天,我现在手上没有持有股票,至今最多进行 3 次交易。很容易理解,对吧?
我们想求的最终答案是 dp
[
n - 1
][
K
][
0],即最后一天,最多允许 K 次交易,最多获得多少利润。读者可能问为什么不是 dp[n - 1
][
K
]
[1]?因为 [1] 代表手上还持有股票,[0] 表示手上的股票已经卖出去了,很显然后者得到的利润一定大于前者。
我们想求的最终答案是
`dp[n - 1][K][0]`
,即最后一天,最多允许
`K`
次交易,最多获得多少利润。
读者可能问为什么不是
`dp[n - 1][K][1]`
?因为
`dp[n - 1][K][1]`
代表到最后一天手上还持有股票,
`dp[n - 1][K][0]`
表示最后一天手上的股票已经卖出去了,很显然后者得到的利润一定大于前者。
记住如何解释「状态」,一旦你觉得哪里不好理解,把它翻译成自然语言就容易理解了。
**二、状态转移框架**
### 二、状态转移框架
现在,我们完成了「状态」的穷举,我们开始思考每种「状态」有哪些「选择」,应该如何更新「状态」。
现在,我们完成了「状态」的穷举,我们开始思考每种「状态」有哪些「选择」,应该如何更新「状态」。只看「持有状态」,可以画个状态转移图。
只看「持有状态」,可以画个状态转移图:
![](
../pictures/%E8%82%A1%E7%A5%A8%E9%97%AE%E9%A2%98/1.png
)
通过这个图可以很清楚地看到,每种状态(0 和 1)是如何转移而来的。根据这个图,我们来写一下状态转移方程:
```
```
python
dp
[
i
][
k
][
0
]
=
max
(
dp
[
i
-
1
][
k
][
0
],
dp
[
i
-
1
][
k
][
1
]
+
prices
[
i
])
max( 选择 rest , 选择 sell )
max
(
今天选择
rest
,
今天选择
sell
)
```
解释:今天我没有持有股票,有两种可能:
要么是我昨天就没有持有,然后今天选择 rest,所以我今天还是没有持有;
要么是我昨天持有股票,但是今天我 sell 了,所以我今天没有持有股票了。
解释:今天我没有持有股票,有两种可能,我从这两种可能中求最大利润:
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
max( 选择 rest , 选择 buy )
1、我昨天就没有持有,且截至昨天最大交易次数限制为
`k`
;然后我今天选择
`rest`
,所以我今天还是没有持有,最大交易次数限制依然为
`k`
。
解释:今天我持有着股票,有两种可能:
要么我昨天就持有着股票,然后今天选择 rest,所以我今天还持有着股票;
要么我昨天本没有持有,但今天我选择 buy,所以今天我就持有股票了。
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
)
```
这个解释应该很清楚了,如果 buy,就要从利润中减去 prices[i],如果 sell,就要给利润增加 prices[i]。今天的最大利润就是这两种可能选择中较大的那个。而且注意 k 的限制,我们在选择 buy 的时候,把 k 减小了 1,很好理解吧,当然你也可以在 sell 的时候减 1,一样的。
解释:今天我持有着股票,最大交易次数限制为
`k`
,那么对于昨天来说,有两种可能,我从这两种可能中求最大利润:
现在,我们已经完成了动态规划中最困难的一步:状态转移方程。
**如果之前的内容你都可以理解,那么你已经可以秒杀所有问题了,只要套这个框架就行了。**
不过还差最后一点点,就是定义 base case,即最简单的情况。
```
dp[-1][k][0] = 0
解释:因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0 。
dp[-1][k][1] = -infinity
解释:还没开始的时候,是不可能持有股票的,用负无穷表示这种不可能。
dp[i][0][0] = 0
解释:因为 k 是从 1 开始的,所以 k = 0 意味着根本不允许交易,这时候利润当然是 0 。
dp[i][0][1] = -infinity
解释:不允许交易的情况下,是不可能持有股票的,用负无穷表示这种不可能。
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
解释
:
不允许交易的情况下
,
是不可能持有股票的
。
因为我们的算法要求一个最大值
,
所以初始值设为一个最小值
,
方便取最大值
。
```
把上面的状态转移方程总结一下:
```
```
python
base
case
:
dp[-1][
k][0] = dp[i
][0][0] = 0
dp[-1][
k][1] = dp[i
][0][1] = -infinity
dp
[
-
1
][
...][
0
]
=
dp
[...
][
0
][
0
]
=
0
dp
[
-
1
][
...][
1
]
=
dp
[...
][
0
][
1
]
=
-
infinity
状态转移方程
:
dp
[
i
][
k
][
0
]
=
max
(
dp
[
i
-
1
][
k
][
0
],
dp
[
i
-
1
][
k
][
1
]
+
prices
[
i
])
...
...
@@ -150,13 +177,13 @@ dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
读者可能会问,这个数组索引是 -1 怎么编程表示出来呢,负无穷怎么表示呢?这都是细节问题,有很多方法实现。现在完整的框架已经完成,下面开始具体化。
**三、秒杀题目**
### 三、秒杀题目
**第一题,k = 1**
直接套状态转移方程,根据 base case,可以做一些化简:
```
```
python
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
])
...
...
@@ -180,34 +207,47 @@ for (int i = 0; i < n; i++) {
return
dp
[
n
-
1
][
0
];
```
显然
i = 0 时 dp[i-1] 是不合法的。这是因为我们没有对 i 的 base case 进行处理。可以这样
处理:
显然
`i = 0`
时
`i - 1`
是不合法的索引,这是因为我们没有对
`i`
的 base case 进行处理,可以这样给一个特化
处理:
```
java
for
(
int
i
=
0
;
i
<
n
;
i
++)
{
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
;
}
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
]);
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
;
}
return
dp
[
n
-
1
][
0
];
```
第一题就解决了,但是这样处理 base case 很麻烦,而且注意一下状态转移方程,新状态只和相邻的一个状态有关,其实不用整个
dp
数组,只需要一个变量储存相邻的那个状态就足够了,这样可以把空间复杂度降到 O(1):
第一题就解决了,但是这样处理 base case 很麻烦,而且注意一下状态转移方程,新状态只和相邻的一个状态有关,其实不用整个
`dp`
数组,只需要一个变量储存相邻的那个状态就足够了,这样可以把空间复杂度降到 O(1):
```
java
// k == 1
// 原始版本
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
];
}
// 空间复杂度优化版本
int
maxProfit_k_1
(
int
[]
prices
)
{
int
n
=
prices
.
length
;
// base case: dp[-1][0] = 0, dp[-1][1] = -infinity
...
...
@@ -222,11 +262,11 @@ int maxProfit_k_1(int[] prices) {
}
```
两种方式都是一样的,不过这种编程方法简洁很多
。但是如果没有前面状态转移方程的引导,是肯定看不懂的。后续的题目,我主要写这种空间复杂度 O(1) 的解法
。
两种方式都是一样的,不过这种编程方法简洁很多
,但是如果没有前面状态转移方程的引导,是肯定看不懂的。后续的题目,你可以对比一下如何把
`dp`
数组的空间优化掉
。
**第二题,k = +infinity**
如果
k 为正无穷,那么就可以认为 k 和 k - 1
是一样的。可以这样改写框架:
如果
`k`
为正无穷,那么就可以认为
`k`
和
`k - 1`
是一样的。可以这样改写框架:
```
python
dp
[
i
][
k
][
0
]
=
max
(
dp
[
i
-
1
][
k
][
0
],
dp
[
i
-
1
][
k
][
1
]
+
prices
[
i
])
...
...
@@ -241,6 +281,24 @@ dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
直接翻译成代码:
```
java
// 原始版本
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
];
}
// 空间复杂度优化版本
int
maxProfit_k_inf
(
int
[]
prices
)
{
int
n
=
prices
.
length
;
int
dp_i_0
=
0
,
dp_i_1
=
Integer
.
MIN_VALUE
;
...
...
@@ -255,9 +313,9 @@ int maxProfit_k_inf(int[] prices) {
**第三题,k = +infinity with cooldown**
每次
sell
之后要等一天才能继续交易。只要把这个特点融入上一题的状态转移方程即可:
每次
`sell`
之后要等一天才能继续交易。只要把这个特点融入上一题的状态转移方程即可:
```
```
python
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
。
...
...
@@ -266,6 +324,35 @@ dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i])
翻译成代码:
```
java
// 原始版本
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
];
}
// 空间复杂度优化版本
int
maxProfit_with_cool
(
int
[]
prices
)
{
int
n
=
prices
.
length
;
int
dp_i_0
=
0
,
dp_i_1
=
Integer
.
MIN_VALUE
;
...
...
@@ -284,16 +371,41 @@ int maxProfit_with_cool(int[] prices) {
每次交易要支付手续费,只要把手续费从利润中减去即可。改写方程:
```
```
python
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
)
解释
:
相当于买入股票的价格升高了
。
在第一个式子里减也是一样的
,
相当于卖出股票的价格减小了
。
```
直接翻译成代码:
> 如果直接把 `fee` 放在第一个式子里减,会有测试用例无法通过,错误原因是整型溢出而不是思路问题。一种解决方案是把代码中的 `int` 类型都改成 `long` 类型,避免 `int` 的整型溢出。
直接翻译成代码,注意状态转移方程改变后 base case 也要做出对应改变:
```
java
// 原始版本
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
];
}
// 空间复杂度优化版本
int
maxProfit_with_fee
(
int
[]
prices
,
int
fee
)
{
int
n
=
prices
.
length
;
int
dp_i_0
=
0
,
dp_i_1
=
Integer
.
MIN_VALUE
;
...
...
@@ -308,12 +420,12 @@ int maxProfit_with_fee(int[] prices, int fee) {
**第五题,k = 2**
k = 2 和前面题目的情况稍微不同,因为上面的情况都和 k 的关系不太大。要么 k 是正无穷,状态转移和 k 没关系了;要么 k = 1,跟 k = 0
这个 base case 挨得近,最后也没有存在感。
`k = 2`
和前面题目的情况稍微不同,因为上面的情况都和
`k`
的关系不太大。要么
`k`
是正无穷,状态转移和
`k`
没关系了;要么
`k = 1`
,跟
`k = 0`
这个 base case 挨得近,最后也没有存在感。
这道题
k = 2 和后面要讲的 k 是任意正整数的情况中,对 k
的处理就凸显出来了。我们直接写代码,边写边分析原因。
这道题
`k = 2`
和后面要讲的
`k`
是任意正整数的情况中,对
`k`
的处理就凸显出来了。我们直接写代码,边写边分析原因。
```
java
原始的
动
态转移方程
,
没有可化简的地方
原始的
状
态转移方程
,
没有可化简的地方
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
])
```
...
...
@@ -323,8 +435,13 @@ 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
];
for
(
int
i
=
0
;
i
<
n
;
i
++)
if
(
i
-
1
==
-
1
)
{
/* 处理一下 base case*/
}
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
;
}
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
]);
}
...
...
@@ -333,37 +450,66 @@ return dp[n - 1][k][0];
为什么错误?我这不是照着状态转移方程写的吗?
还记得前面总结的「穷举框架」吗?就是说我们必须穷举所有状态。其实我们之前的解法,都在穷举所有状态,只是之前的题目中
k 都被化简掉了。比如说第一题,k = 1:
还记得前面总结的「穷举框架」吗?就是说我们必须穷举所有状态。其实我们之前的解法,都在穷举所有状态,只是之前的题目中
`k`
都被化简掉了。
「代码截图」
这道题由于没有消掉 k 的影响,所以必须要对 k 进行穷举:
比如说第一题,
`k = 1`
时的代码框架:
```
java
int
max_k
=
2
;
int
[][]
[]
dp
=
new
int
[
n
][
max_k
+
1
][
2
];
int
n
=
prices
.
length
;
int
[][]
dp
=
new
int
[
n
][
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
]
=
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
]);
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
]);
}
}
// 穷举了 n × max_k × 2 个状态,正确。
return
dp
[
n
-
1
][
max_k
][
0
];
}
// 穷举了 n × max_k × 2 个状态,正确。
return
dp
[
n
-
1
][
max_k
][
0
];
```
如果你不理解,可以返回第一点「穷举框架」重新阅读体会一下
。
> **PS:这里肯定会有读者疑惑,`k` 的 base case 是 0,按理说应该从 `k = 1, k++` 这样穷举状态 `k` 才对?而且如果你真的这样从小到大遍历 `k`,提交发现也是可以的**
。
这里 k 取值范围比较小,所以可以不用 for 循环,直接把 k = 1 和 2 的情况全部列举出来也可以:
这个疑问很正确,因为我们前文
[
动态规划答疑篇
](
../动态规划系列/最优子结构.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 的情况全部列举出来也可以:
```
java
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
])
// 状态转移方程:
// 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])
// 空间复杂度优化版本
int
maxProfit_k_2
(
int
[]
prices
)
{
// base case
int
dp_i10
=
0
,
dp_i11
=
Integer
.
MIN_VALUE
;
int
dp_i20
=
0
,
dp_i21
=
Integer
.
MIN_VALUE
;
for
(
int
price
:
prices
)
{
...
...
@@ -376,28 +522,47 @@ int maxProfit_k_2(int[] prices) {
}
```
有状态转移方程和含义明确的变量名指导,相信你很容易看懂。其实我们可以故弄玄虚,把上述四个变量换成
a, b, c, d
。这样当别人看到你的代码时就会大惊失色,对你肃然起敬。
有状态转移方程和含义明确的变量名指导,相信你很容易看懂。其实我们可以故弄玄虚,把上述四个变量换成
`a, b, c, d`
。这样当别人看到你的代码时就会大惊失色,对你肃然起敬。
**第六题,k = any integer**
有了上一题
k = 2 的铺垫,这题应该和上一题的第一个解法没啥区别。但是出现了一个超内存的错误,原来是传入的 k 值会非常大,dp 数组太大了。现在想想,交易次数 k
最多有多大呢?
有了上一题
`k = 2`
的铺垫,这题应该和上一题的第一个解法没啥区别。但是出现了一个超内存的错误,原来是传入的
`k`
值会非常大,
`dp`
数组太大了。现在想想,交易次数
`k`
最多有多大呢?
一次交易由买入和卖出构成,至少需要两天。所以说有效的限制
k 应该不超过 n/2,如果超过,就没有约束作用了,相当于 k = +infinity
。这种情况是之前解决过的。
一次交易由买入和卖出构成,至少需要两天。所以说有效的限制
`k`
应该不超过
`n/2`
,如果超过,就没有约束作用了,相当于
`k = +infinity`
。这种情况是之前解决过的。
直接把之前的代码重用:
```
java
int
maxProfit_k_any
(
int
max_k
,
int
[]
prices
)
{
int
n
=
prices
.
length
;
if
(
max_k
>
n
/
2
)
if
(
n
<=
0
)
{
return
0
;
}
if
(
max_k
>
n
/
2
)
{
// 交易次数 k 没有限制的情况
return
maxProfit_k_inf
(
prices
);
}
// base case:
// dp[-1][...][0] = dp[...][0][0] = 0
// dp[-1][...][1] = dp[...][0][1] = -infinity
int
[][][]
dp
=
new
int
[
n
][
max_k
+
1
][
2
];
// k = 0 时的 base case
for
(
int
i
=
0
;
i
<
n
;
i
++)
{
dp
[
i
][
0
][
1
]
=
Integer
.
MIN_VALUE
;
dp
[
i
][
0
][
0
]
=
0
;
}
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
]
=
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
]);
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
]);
}
return
dp
[
n
-
1
][
max_k
][
0
];
}
...
...
@@ -405,17 +570,14 @@ int maxProfit_k_any(int max_k, int[] prices) {
至此,6 道题目通过一个状态转移方程全部解决。
**四、最后总结**
本文给大家讲了如何通过状态转移的方法解决复杂的问题,用一个状态转移方程秒杀了 6 道股票买卖问题,现在想想,其实也不算难对吧?这已经属于动态规划问题中较困难的了。
关键就在于列举出所有可能的「状态」,然后想想怎么穷举更新这些「状态」。一般用一个多维
dp
数组储存这些状态,从 base case 开始向后推进,推进到最后的状态,就是我们想要的答案。想想这个过程,你是不是有点理解「动态规划」这个名词的意义了呢?
关键就在于列举出所有可能的「状态」,然后想想怎么穷举更新这些「状态」。一般用一个多维
`dp`
数组储存这些状态,从 base case 开始向后推进,推进到最后的状态,就是我们想要的答案。想想这个过程,你是不是有点理解「动态规划」这个名词的意义了呢?
具体到股票买卖问题,我们发现了三个状态,使用了一个三维数组,无非还是穷举 + 更新,不过我们可以说的高大上一点,这叫「三维 DP」,怕不怕?这个大实话一说,立刻显得你高人一等,名利双收有没有,所以给个在看/分享吧,鼓励一下我。
**_____________**
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
。
...
...
@@ -425,385 +587,3 @@ int maxProfit_k_any(int max_k, int[] prices) {
<p
align=
'center'
>
<img
src=
"../pictures/qrcode.jpg"
width=
200
>
</p>
======其他语言代码======
[
买卖股票的最佳时机
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock
)
[
买卖股票的最佳时机 II
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/
)
[
买卖股票的最佳时机 III
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/
)
[
买卖股票的最佳时机 IV
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/
)
[
最佳买卖股票时机含冷冻期
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/
)
[
买卖股票的最佳时机含手续费
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/
)
### javascript
**第一题**
[
买卖股票的最佳时机
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock
)
,相当于
`k=1`
的情形。
```
js
var
maxProfit
=
function
(
prices
)
{
let
n
=
prices
.
length
;
if
(
n
<=
1
)
{
return
0
;
}
let
dp
=
new
Array
(
n
);
dp
.
fill
([
0
,
0
],
0
,
n
)
// base case
// 解释:
// dp[i][0]
// = max(dp[-1][0], dp[-1][1] + prices[i])
// = max(0, -infinity + prices[i]) = 0
// dp[0][0] = 0;
// 解释:
// dp[i][1]
// = max(dp[-1][1], dp[-1][0] - prices[i])
// = max(-infinity, 0 - prices[i])
// = -prices[i]
dp
[
0
][
1
]
=
-
prices
[
0
];
// 状态转移
for
(
let
i
=
1
;
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
]
};
```
状态压缩
```
js
var
maxProfit
=
function
(
prices
)
{
let
n
=
prices
.
length
;
// base case
let
dp_i_0
=
0
,
dp_i_1
=
-
prices
[
0
];
for
(
let
i
=
1
;
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
;
}
```
**第二题**
[
买卖股票的最佳时机 II
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/
)
,相当于
`k = +infinity`
的情形。
```
js
/**
* @param {number[]} prices
* @return {number}
*/
var
maxProfit
=
function
(
prices
)
{
let
n
=
prices
.
length
;
let
dp
=
new
Array
(
n
);
dp
.
fill
([
0
,
0
],
0
,
n
)
dp
[
0
][
0
]
=
0
;
dp
[
0
][
1
]
=
-
prices
[
0
];
for
(
let
i
=
1
;
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
],
dp
[
i
-
1
][
0
]
-
prices
[
i
]
)
}
return
dp
[
n
-
1
][
0
]
};
```
状态压缩
```
js
/**
* @param {number[]} prices
* @return {number}
*/
var
maxProfit
=
function
(
prices
)
{
let
n
=
prices
.
length
;
// base case
let
dp_i_0
=
0
,
dp_i_1
=
-
prices
[
0
];
for
(
let
i
=
0
;
i
<
n
;
i
++
)
{
// dp[i][0] = Math.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] = Math.max(
// dp[i - 1][1],
// dp[i - 1][0] - prices[i]
// )
dp_i_1
=
Math
.
max
(
dp_i_1
,
dp_i_0
-
prices
[
i
])
}
return
dp_i_0
;
};
```
**第三题**
[
最佳买卖股票时机含冷冻期
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/
)
,相当于
`k = +infinity with cooldown`
的情形。
-
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
-
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
```
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 。
```
```
js
/**
* @param {number[]} prices
* @return {number}
*/
var
maxProfit
=
function
(
prices
)
{
let
n
=
prices
.
length
;
if
(
n
<
2
)
{
return
0
;
}
if
(
n
===
2
)
{
return
Math
.
max
(
prices
[
1
]
-
prices
[
0
],
0
)
}
let
dp
=
new
Array
(
n
);
for
(
let
i
=
0
;
i
<
n
;
i
++
)
{
dp
[
i
]
=
[
0
,
0
]
}
// base case
// dp[0][0] = 0;
dp
[
0
][
1
]
=
-
prices
[
0
];
dp
[
1
][
0
]
=
Math
.
max
(
dp
[
0
][
0
],
dp
[
0
][
1
]
+
prices
[
1
]
)
dp
[
1
][
1
]
=
Math
.
max
(
dp
[
0
][
1
],
dp
[
0
][
0
]
-
prices
[
1
]
);
// 状态转移
for
(
let
i
=
2
;
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
],
dp
[
i
-
2
][
0
]
-
prices
[
i
]
// 买被限制在卖一天后了
)
}
return
dp
[
n
-
1
][
0
];
};
```
状态压缩
```
js
/**
* @param {number[]} prices
* @return {number}
*/
var
maxProfit
=
function
(
prices
)
{
let
n
=
prices
.
length
;
let
dp_i_0
=
0
;
let
dp_i_1
=
-
Infinity
;
// 还未买入
let
dp_pre_0
=
0
;
// 代表 dp[i-2][0]
for
(
let
i
=
0
;
i
<
n
;
i
++
)
{
let
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
;
};
```
**第四题**
[
买卖股票的最佳时机含手续费
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/
)
。
`k = +infinity with fee`
的情形。
每次交易要支付手续费,只要把手续费从利润中减去即可。
```
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)
解释:相当于买入股票的价格升高了。
在第一个式子里减也是一样的,相当于卖出股票的价格减小了。
```
```
js
/**
* @param {number[]} prices
* @param {number} fee
* @return {number}
*/
var
maxProfit
=
function
(
prices
,
fee
)
{
let
n
=
prices
.
length
;
let
dp
=
new
Array
(
n
);
for
(
let
i
=
0
;
i
<
n
;
i
++
)
{
dp
[
i
]
=
[
0
,
0
]
}
// base case
// dp[0][0] = 0;
dp
[
0
][
1
]
=
-
prices
[
0
]
-
fee
;
// 状态转移
for
(
let
i
=
1
;
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
],
dp
[
i
-
1
][
0
]
-
prices
[
i
]
-
fee
// 相当于买入股票的价格升高了
)
}
return
dp
[
n
-
1
][
0
];
};
```
状态压缩
```
js
var
maxProfit
=
function
(
prices
,
fee
)
{
let
n
=
prices
.
length
;
// base case
let
dp_i_0
=
0
,
dp_i_1
=
-
prices
[
0
]
-
fee
;
for
(
let
i
=
0
;
i
<
n
;
i
++
)
{
dp_i_0
=
Math
.
max
(
dp_i_0
,
dp_i_1
+
prices
[
i
]);
dp_i_1
=
Math
.
max
(
dp_i_1
,
dp_i_0
-
prices
[
i
]
-
fee
)
}
return
dp_i_0
;
};
```
**第五题**
[
买卖股票的最佳时机 III
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/
)
。
`k = 2 `
的情形。
```
dp[-1][k][0] = 0
解释:因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0 。
dp[-1][k][1] = -infinity
解释:还没开始的时候,是不可能持有股票的,用负无穷表示这种不可能。
dp[i][0][0] = 0
解释:因为 k 是从 1 开始的,所以 k = 0 意味着根本不允许交易,这时候利润当然是 0 。
dp[i][0][1] = -infinity
解释:不允许交易的情况下,是不可能持有股票的,用负无穷表示这种不可能。
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
max( 选择 rest , 选择 sell )
解释:今天我没有持有股票,有两种可能:
要么是我昨天就没有持有,然后今天选择 rest,所以我今天还是没有持有;
要么是我昨天持有股票,但是今天我 sell 了,所以我今天没有持有股票了。
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
max( 选择 rest , 选择 buy )
解释:今天我持有着股票,有两种可能:
要么我昨天就持有着股票,然后今天选择 rest,所以我今天还持有着股票;
要么我昨天本没有持有,但今天我选择 buy,所以今天我就持有股票了。
```
```
js
var
maxProfit
=
function
(
prices
)
{
//第一次 买入, 卖出的利润
let
profit_1_in
=
-
prices
[
0
],
profit_1_out
=
0
;
//继第一次之后,第二次买入卖出的利润
let
profit_2_in
=
-
prices
[
0
],
profit_2_out
=
0
;
let
n
=
prices
.
length
;
for
(
let
i
=
1
;
i
<
n
;
i
++
){
profit_2_out
=
Math
.
max
(
profit_2_out
,
profit_2_in
+
prices
[
i
]);
//第二次买入后的利润, 第一次卖出的利润 - prices[i]
profit_2_in
=
Math
.
max
(
profit_2_in
,
profit_1_out
-
prices
[
i
]);
profit_1_out
=
Math
.
max
(
profit_1_out
,
profit_1_in
+
prices
[
i
]);
//第一次买入后,利润为 -prices[i]
profit_1_in
=
Math
.
max
(
profit_1_in
,
-
prices
[
i
]);
}
return
profit_2_out
;
};
```
**第六题**
[
买卖股票的最佳时机 IV
](
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/
)
。k = any integer的情形。
```
js
/**
* @param {number} k
* @param {number[]} prices
* @return {number}
*/
var
maxProfit
=
function
(
k
,
prices
)
{
if
(
!
prices
.
length
)
{
return
0
;
}
const
n
=
prices
.
length
;
k
=
Math
.
min
(
k
,
Math
.
floor
(
n
/
2
));
const
buy
=
new
Array
(
k
+
1
).
fill
(
0
);
const
sell
=
new
Array
(
k
+
1
).
fill
(
0
);
buy
[
0
]
=
-
prices
[
0
]
sell
[
0
]
=
0
for
(
let
i
=
1
;
i
<
k
+
1
;
++
i
)
{
buy
[
i
]
=
sell
[
i
]
=
-
Number
.
MAX_VALUE
;
}
for
(
let
i
=
1
;
i
<
n
;
++
i
)
{
buy
[
0
]
=
Math
.
max
(
buy
[
0
],
sell
[
0
]
-
prices
[
i
]);
for
(
let
j
=
1
;
j
<
k
+
1
;
++
j
)
{
buy
[
j
]
=
Math
.
max
(
buy
[
j
],
sell
[
j
]
-
prices
[
i
]);
sell
[
j
]
=
Math
.
max
(
sell
[
j
],
buy
[
j
-
1
]
+
prices
[
i
]);
}
}
return
Math
.
max
(...
sell
)
};
```
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录