Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ Gitee Pages 地址:https://labuladong.gitee.io/algo/
* [动态规划设计:最长递增子序列](https://labuladong.github.io/article/fname.html?fname=动态规划设计:最长递增子序列)
* [最优子结构原理和 dp 数组遍历方向](https://labuladong.github.io/article/fname.html?fname=最优子结构)
* [base case 和备忘录的初始值怎么定?](https://labuladong.github.io/article/fname.html?fname=备忘录等基础)
* [动态规划穷举的两种视角](https://labuladong.github.io/article/fname.html?fname=动归两种视角)
* [对动态规划进行降维打击](https://labuladong.github.io/article/fname.html?fname=状态压缩技巧)

* [子序列类型问题](https://labuladong.github.io/algo/)
Expand Down
6 changes: 3 additions & 3 deletions 动态规划系列/动态规划之KMP字符匹配算法.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

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

> 阅读本文之前,建议你先学习一下另一种字符串匹配算法:[Rabin Karp 字符匹配算法](https://labuladong.github.io/article/fname.html?fname=rabinkarp)。
> tip:阅读本文之前,建议你先学习一下另一种字符串匹配算法:[Rabin Karp 字符匹配算法](https://labuladong.github.io/article/fname.html?fname=rabinkarp)。

KMP 算法(Knuth-Morris-Pratt 算法)是一个著名的字符串匹配算法,效率很高,但是确实有点复杂。

Expand All @@ -33,7 +33,7 @@ KMP 算法(Knuth-Morris-Pratt 算法)是一个著名的字符串匹配算法

读者见过的 KMP 算法应该是,一波诡异的操作处理 `pat` 后形成一个一维的数组 `next`,然后根据这个数组经过又一波复杂操作去匹配 `txt`。时间复杂度 O(N),空间复杂度 O(M)。其实它这个 `next` 数组就相当于 `dp` 数组,其中元素的含义跟 `pat` 的前缀和后缀有关,判定规则比较复杂,不好理解。**本文则用一个二维的 `dp` 数组(但空间复杂度还是 O(M)),重新定义其中元素的含义,使得代码长度大大减少,可解释性大大提高**。

> PS:本文的代码参考《算法4》,原代码使用的数组名称是 `dfa`(确定有限状态机),因为我们的公众号之前有一系列动态规划的文章,就不说这么高大上的名词了,我对书中代码进行了一点修改,并沿用 `dp` 数组的名称。
> note:本文的代码参考《算法4》,原代码使用的数组名称是 `dfa`(确定有限状态机),因为我们的公众号之前有一系列动态规划的文章,就不说这么高大上的名词了,我对书中代码进行了一点修改,并沿用 `dp` 数组的名称。

### 一、KMP 算法概述

Expand Down Expand Up @@ -103,7 +103,7 @@ pat = "aaab"

![](https://labuladong.gitee.io/pictures/kmp/txt2.jpg)

> PS:这个`j` 不要理解为索引,它的含义更准确地说应该是**状态**(state),所以它会出现这个奇怪的位置,后文会详述。
> note:这个`j` 不要理解为索引,它的含义更准确地说应该是**状态**(state),所以它会出现这个奇怪的位置,后文会详述。

而对于 `txt2` 的下面这个即将出现的未匹配情况:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ int lengthOfLIS(int[] nums);

**我们的定义是这样的:`dp[i]` 表示以 `nums[i]` 这个数结尾的最长递增子序列的长度**。

> PS:为什么这样定义呢?这是解决子序列问题的一个套路,后文 [动态规划之子序列问题解题模板](https://labuladong.github.io/article/fname.html?fname=子序列问题模板) 总结了几种常见套路。你读完本章所有的动态规划问题,就会发现 `dp` 数组的定义方法也就那几种。
> info:为什么这样定义呢?这是解决子序列问题的一个套路,后文 [动态规划之子序列问题解题模板](https://labuladong.github.io/article/fname.html?fname=子序列问题模板) 总结了几种常见套路。你读完本章所有的动态规划问题,就会发现 `dp` 数组的定义方法也就那几种。

根据这个定义,我们就可以推出 base case:`dp[i]` 初始值为 1,因为以 `nums[i]` 结尾的最长递增子序列起码要包含它自己。

Expand Down Expand Up @@ -182,7 +182,7 @@ int lengthOfLIS(int[] nums) {

我们只要把处理扑克牌的过程编程写出来即可。每次处理一张扑克牌不是要找一个合适的牌堆顶来放吗,牌堆顶的牌不是**有序**吗,这就能用到二分查找了:用二分查找来搜索当前牌应放置的位置。

> PS:前文 [二分查找算法详解](https://labuladong.github.io/article/fname.html?fname=二分查找详解) 详细介绍了二分查找的细节及变体,这里就完美应用上了,如果没读过强烈建议阅读。
> tip:前文 [二分查找算法详解](https://labuladong.github.io/article/fname.html?fname=二分查找详解) 详细介绍了二分查找的细节及变体,这里就完美应用上了,如果没读过强烈建议阅读。

```java
int lengthOfLIS(int[] nums) {
Expand Down Expand Up @@ -298,9 +298,9 @@ int lengthOfLIS(int[] nums) {

- [二分查找高效判定子序列](https://labuladong.github.io/article/fname.html?fname=二分查找判定子序列)
- [动态规划之子序列问题解题模板](https://labuladong.github.io/article/fname.html?fname=子序列问题模板)
- [动态规划穷举的两种视角](https://labuladong.github.io/article/fname.html?fname=动归两种视角)
- [动态规划解题套路框架](https://labuladong.github.io/article/fname.html?fname=动态规划详解进阶)
- [动态规划设计:最大子数组](https://labuladong.github.io/article/fname.html?fname=最大子数组)
- [动态规划问题的两种穷举视角](https://labuladong.github.io/article/fname.html?fname=动归两种视角)
- [我的刷题心得](https://labuladong.github.io/article/fname.html?fname=算法心得)
- [最优子结构原理和 dp 数组遍历方向](https://labuladong.github.io/article/fname.html?fname=最优子结构)

Expand Down
14 changes: 7 additions & 7 deletions 动态规划系列/动态规划详解进阶.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

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

> 本文有视频版:[动态规划框架套路详解](https://www.bilibili.com/video/BV1XV411Y7oE)。建议关注我的 B 站账号,我会用视频领读的方式带大家学习那些稍有难度的算法技巧。
> tip:本文有视频版:[动态规划框架套路详解](https://www.bilibili.com/video/BV1XV411Y7oE)。建议关注我的 B 站账号,我会用视频领读的方式带大家学习那些稍有难度的算法技巧。

这篇文章是我们公众号半年前一篇 200 多赞赏的成名之作 [动态规划详解](https://mp.weixin.qq.com/s/1V3aHVonWBEXlNUvK3S28w) 的进阶版。由于账号迁移的原因,旧文无法被搜索到,所以我润色了本文,并添加了更多干货内容,希望本文成为解决动态规划的一部「指导方针」。

Expand Down Expand Up @@ -88,7 +88,7 @@ int fib(int N) {

![](https://labuladong.gitee.io/pictures/动态规划详解进阶/1.jpg)

> PS:但凡遇到需要递归的问题,最好都画出递归树,这对你分析算法的复杂度,寻找算法低效的原因都有巨大帮助。
> tip:但凡遇到需要递归的问题,最好都画出递归树,这对你分析算法的复杂度,寻找算法低效的原因都有巨大帮助。

这个递归树怎么理解?就是说想要计算原问题 `f(20)`,我就得先计算出子问题 `f(19)` 和 `f(18)`,然后要计算 `f(19)`,我就要先算出子问题 `f(18)` 和 `f(17)`,以此类推。最后遇到 `f(1)` 或者 `f(2)` 的时候,结果已知,就能直接返回结果,递归树不再向下生长了。

Expand Down Expand Up @@ -249,7 +249,7 @@ int coinChange(int[] coins, int amount);

回到凑零钱问题,为什么说它符合最优子结构呢?假设你有面值为 `1, 2, 5` 的硬币,你想求 `amount = 11` 时的最少硬币数(原问题),如果你知道凑出 `amount = 10, 9, 6` 的最少硬币数(子问题),你只需要把子问题的答案加一(再选一枚面值为 `1, 2, 5` 的硬币),求个最小值,就是原问题的答案。因为硬币的数量是没有限制的,所以子问题之间没有相互制,是互相独立的。

> PS:关于最优子结构的问题,后文 [动态规划答疑篇](https://labuladong.github.io/article/fname.html?fname=最优子结构) 还会再举例探讨。
> tip:关于最优子结构的问题,后文 [动态规划答疑篇](https://labuladong.github.io/article/fname.html?fname=最优子结构) 还会再举例探讨。

那么,既然知道了这是个动态规划问题,就要思考如何列出正确的状态转移方程?

Expand Down Expand Up @@ -310,7 +310,7 @@ int dp(int[] coins, int amount) {
}
```

> PS:这里 `coinChange` 和 `dp` 函数的签名完全一样,所以理论上不需要额外写一个 `dp` 函数。但为了后文讲解方便,这里还是另写一个 `dp` 函数来实现主要逻辑。
> note:这里 `coinChange` 和 `dp` 函数的签名完全一样,所以理论上不需要额外写一个 `dp` 函数。但为了后文讲解方便,这里还是另写一个 `dp` 函数来实现主要逻辑。

> 另外,我经常看到有人问,子问题的结果为什么要加 1(`subProblem + 1`),而不是加硬币金额之类的。我这里统一提示一下,动态规划问题的关键是 `dp` 函数/数组的定义,你这个函数的返回值代表什么?你回过头去搞清楚这一点,然后就知道为什么要给子问题的返回值加 1 了。

Expand Down Expand Up @@ -400,9 +400,9 @@ int coinChange(int[] coins, int amount) {
}
```

![](https://labuladong.gitee.io/pictures/动态规划详解进阶/6.jpg)
> info:为啥 `dp` 数组中的值都初始化为 `amount + 1` 呢,因为凑成 `amount` 金额的硬币数最多只可能等于 `amount`(全用 1 元面值的硬币),所以初始化为 `amount + 1` 就相当于初始化为正无穷,便于后续取最小值。为啥不直接初始化为 int 型的最大值 `Integer.MAX_VALUE` 呢?因为后面有 `dp[i - coin] + 1`,这就会导致整型溢出。

> PS:为啥 `dp` 数组中的值都初始化为 `amount + 1` 呢,因为凑成 `amount` 金额的硬币数最多只可能等于 `amount`(全用 1 元面值的硬币),所以初始化为 `amount + 1` 就相当于初始化为正无穷,便于后续取最小值。为啥不直接初始化为 int 型的最大值 `Integer.MAX_VALUE` 呢?因为后面有 `dp[i - coin] + 1`,这就会导致整型溢出。
![](https://labuladong.gitee.io/pictures/动态规划详解进阶/6.jpg)

### 三、最后总结

Expand Down Expand Up @@ -439,8 +439,8 @@ int coinChange(int[] coins, int amount) {
- [分治算法详解:运算优先级](https://labuladong.github.io/article/fname.html?fname=分治算法)
- [动态规划帮我通关了《辐射4》](https://labuladong.github.io/article/fname.html?fname=转盘)
- [动态规划帮我通关了《魔塔》](https://labuladong.github.io/article/fname.html?fname=魔塔)
- [动态规划穷举的两种视角](https://labuladong.github.io/article/fname.html?fname=动归两种视角)
- [动态规划设计:最长递增子序列](https://labuladong.github.io/article/fname.html?fname=动态规划设计:最长递增子序列)
- [动态规划问题的两种穷举视角](https://labuladong.github.io/article/fname.html?fname=动归两种视角)
- [如何运用贪心思想玩跳跃游戏](https://labuladong.github.io/article/fname.html?fname=跳跃游戏)
- [学习算法和刷题的框架思维](https://labuladong.github.io/article/fname.html?fname=学习数据结构和算法的高效方法)
- [对动态规划进行降维打击](https://labuladong.github.io/article/fname.html?fname=状态压缩技巧)
Expand Down
8 changes: 4 additions & 4 deletions 动态规划系列/团灭股票问题.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,13 @@ dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])

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

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

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

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

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

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

Expand Down Expand Up @@ -386,7 +386,7 @@ dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee)
在第一个式子里减也是一样的,相当于卖出股票的价格减小了。
```

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

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

Expand Down Expand Up @@ -498,7 +498,7 @@ int maxProfit_k_2(int[] prices) {
}
```

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

这个疑问很正确,因为我们前文 [动态规划答疑篇](https://labuladong.github.io/article/fname.html?fname=最优子结构) 有介绍 `dp` 数组的遍历顺序是怎么确定的,主要是根据 base case,以 base case 为起点,逐步向结果靠近。

Expand Down
2 changes: 1 addition & 1 deletion 动态规划系列/最优子结构.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

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

> 本文有视频版:[动态规划详解进阶](https://www.bilibili.com/video/BV1uv411W73P/)。建议关注我的 B 站账号,我会用视频领读的方式带大家学习那些稍有难度的算法技巧。
> tip:本文有视频版:[动态规划详解进阶](https://www.bilibili.com/video/BV1uv411W73P/)。建议关注我的 B 站账号,我会用视频领读的方式带大家学习那些稍有难度的算法技巧。

本文是两年前发的 [动态规划答疑篇](https://mp.weixin.qq.com/s/qvlfyKBiXVX7CCwWFR-XKg) 的修订版,根据我的不断学习总结以及读者的评论反馈,我给扩展了更多内容,力求使本文成为继 [动态规划核心套路框架](https://labuladong.github.io/article/fname.html?fname=动态规划详解进阶) 之后的一篇全面答疑文章。以下是正文。

Expand Down
4 changes: 2 additions & 2 deletions 动态规划系列/状态压缩技巧.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

但是,动态规划求解的过程中也是可以进行阶段性优化的,如果你认真观察某些动态规划问题的状态转移方程,就能够把它们解法的空间复杂度进一步降低,由 O(N^2) 降低到 O(N)。

> PS:之前我在本文中误用了「状态压缩」这个词,有读者指出「状态压缩」这个词的含义是把多个状态通过二进制运算用一个整数表示出来,从而减少 `dp` 数组的维度。而本文描述的优化方式是通过观察状态转移方程的依赖关系,从而减少 `dp` 数组的维度,确实和「状态压缩」有所区别。所以严谨起见,我把原来文章中的「状态压缩」都改为了「空间压缩」,避免名词的误用。
> note:之前我在本文中误用了「状态压缩」这个词,有读者指出「状态压缩」这个词的含义是把多个状态通过二进制运算用一个整数表示出来,从而减少 `dp` 数组的维度。而本文描述的优化方式是通过观察状态转移方程的依赖关系,从而减少 `dp` 数组的维度,确实和「状态压缩」有所区别。所以严谨起见,我把原来文章中的「状态压缩」都改为了「空间压缩」,避免名词的误用。

能够使用空间压缩技巧的动态规划都是二维 `dp` 问题,**你看它的状态转移方程,如果计算状态 `dp[i][j]` 需要的都是 `dp[i][j]` 相邻的状态,那么就可以使用空间压缩技巧**,将二维的 `dp` 数组转化成一维,将空间复杂度从 O(N^2) 降低到 O(N)。

Expand Down Expand Up @@ -50,7 +50,7 @@ int longestPalindromeSubseq(string s) {
}
```

> PS:我们本文不探讨如何推状态转移方程,只探讨对二维 DP 问题进行空间压缩的技巧。技巧都是通用的,所以如果你没看过前文,不明白这段代码的逻辑也无妨,完全不会阻碍你学会空间压缩。
> tip:我们本文不探讨如何推状态转移方程,只探讨对二维 DP 问题进行空间压缩的技巧。技巧都是通用的,所以如果你没看过前文,不明白这段代码的逻辑也无妨,完全不会阻碍你学会空间压缩。

你看我们对 `dp[i][j]` 的更新,其实只依赖于 `dp[i+1][j-1], dp[i][j-1], dp[i+1][j]` 这三个状态:

Expand Down
4 changes: 2 additions & 2 deletions 动态规划系列/编辑距离.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

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

> 本文有视频版:[编辑距离详解动态规划](https://www.bilibili.com/video/BV1uv411W73P/)。建议关注我的 B 站账号,我会用视频领读的方式带大家学习那些稍有难度的算法技巧。
> tip:本文有视频版:[编辑距离详解动态规划](https://www.bilibili.com/video/BV1uv411W73P/)。建议关注我的 B 站账号,我会用视频领读的方式带大家学习那些稍有难度的算法技巧。

前几天看了一份鹅厂的面试题,算法部分大半是动态规划,最后一题就是写一个计算编辑距离的函数,今天就专门写一篇文章来探讨一下这个问题。

Expand Down Expand Up @@ -51,7 +51,7 @@ int minDistance(String s1, String s2)

前文 [最长公共子序列](https://labuladong.github.io/article/fname.html?fname=LCS) 说过,**解决两个字符串的动态规划问题,一般都是用两个指针 `i, j` 分别指向两个字符串的最后,然后一步步往前移动,缩小问题的规模**。

> PS:其实让 `i, j` 从前往后移动也可以,改一下 `dp` 函数/数组的定义即可,思路是完全一样的。
> tip:其实让 `i, j` 从前往后移动也可以,改一下 `dp` 函数/数组的定义即可,思路是完全一样的。

设两个字符串分别为 `"rad"` 和 `"apple"`,为了把 `s1` 变成 `s2`,算法会这样进行:

Expand Down
Loading