Skip to content

Commit 68d6053

Browse files
committed
同步网站新增功能及文章
1 parent c8fb370 commit 68d6053

File tree

93 files changed

+11659
-7669
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+11659
-7669
lines changed

README.md

Lines changed: 148 additions & 158 deletions
Large diffs are not rendered by default.

动态规划系列/LCS.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# 详解最长公共子序列问题,秒杀三道动态规划题目
2+
3+
<p align='center'>
4+
<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>
5+
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
6+
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%[email protected]?style=flat-square&logo=Zhihu"></a>
7+
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站[email protected]?style=flat-square&logo=Bilibili"></a>
8+
</p>
9+
10+
![](https://labuladong.github.io/algo/images/souyisou1.png)
11+
12+
**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
13+
14+
15+
16+
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
17+
18+
| LeetCode | 力扣 | 难度 |
19+
| :----: | :----: | :----: |
20+
| [1143. Longest Common Subsequence](https://leetcode.com/problems/longest-common-subsequence/) | [1143. 最长公共子序列](https://leetcode.cn/problems/longest-common-subsequence/) | 🟠
21+
| [583. Delete Operation for Two Strings](https://leetcode.com/problems/delete-operation-for-two-strings/) | [583. 两个字符串的删除操作](https://leetcode.cn/problems/delete-operation-for-two-strings/) | 🟠
22+
| [712. Minimum ASCII Delete Sum for Two Strings](https://leetcode.com/problems/minimum-ascii-delete-sum-for-two-strings/) | [712. 两个字符串的最小ASCII删除和](https://leetcode.cn/problems/minimum-ascii-delete-sum-for-two-strings/) | 🟠
23+
| - | [剑指 Offer II 095. 最长公共子序列](https://leetcode.cn/problems/qJnOS7/) | 🟠
24+
25+
**-----------**
26+
27+
不知道大家做算法题有什么感觉,**我总结出来做算法题的技巧就是,把大的问题细化到一个点,先研究在这个小的点上如何解决问题,然后再通过递归/迭代的方式扩展到整个问题**
28+
29+
比如说我们前文 [手把手带你刷二叉树第三期](https://labuladong.github.io/article/fname.html?fname=二叉树系列3),解决二叉树的题目,我们就会把整个问题细化到某一个节点上,想象自己站在某个节点上,需要做什么,然后套二叉树递归框架就行了。
30+
31+
动态规划系列问题也是一样,尤其是子序列相关的问题。**本文从「最长公共子序列问题」展开,总结三道子序列问题**,解这道题仔细讲讲这种子序列问题的套路,你就能感受到这种思维方式了。
32+
33+
### 最长公共子序列
34+
35+
计算最长公共子序列(Longest Common Subsequence,简称 LCS)是一道经典的动态规划题目,力扣第 1143 题「最长公共子序列」就是这个问题:
36+
37+
给你输入两个字符串 `s1``s2`,请你找出他们俩的最长公共子序列,返回这个子序列的长度。函数签名如下:
38+
39+
```java
40+
int longestCommonSubsequence(String s1, String s2);
41+
```
42+
43+
比如说输入 `s1 = "zabcde", s2 = "acez"`,它俩的最长公共子序列是 `lcs = "ace"`,长度为 3,所以算法返回 3。
44+
45+
如果没有做过这道题,一个最简单的暴力算法就是,把 `s1``s2` 的所有子序列都穷举出来,然后看看有没有公共的,然后在所有公共子序列里面再寻找一个长度最大的。
46+
47+
显然,这种思路的复杂度非常高,你要穷举出所有子序列,这个复杂度就是指数级的,肯定不实际。
48+
49+
正确的思路是不要考虑整个字符串,而是细化到 `s1``s2` 的每个字符。前文 [子序列解题模板](https://labuladong.github.io/article/fname.html?fname=子序列问题模板) 中总结的一个规律:
50+
51+
52+
53+
<hr>
54+
<details>
55+
<summary><strong>引用本文的文章</strong></summary>
56+
57+
- [动态规划之子序列问题解题模板](https://labuladong.github.io/article/fname.html?fname=子序列问题模板)
58+
- [经典动态规划:编辑距离](https://labuladong.github.io/article/fname.html?fname=编辑距离)
59+
60+
</details><hr>
61+
62+
63+
64+
65+
<hr>
66+
<details>
67+
<summary><strong>引用本文的题目</strong></summary>
68+
69+
<strong>安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:</strong>
70+
71+
| LeetCode | 力扣 |
72+
| :----: | :----: |
73+
| [97. Interleaving String](https://leetcode.com/problems/interleaving-string/?show=1) | [97. 交错字符串](https://leetcode.cn/problems/interleaving-string/?show=1) |
74+
| - | [剑指 Offer II 095. 最长公共子序列](https://leetcode.cn/problems/qJnOS7/?show=1) |
75+
76+
</details>
77+
78+
79+
80+
**_____________**
81+
82+
应合作方要求,本文不便在此发布,请扫码关注回复关键词「LCS」或 [点这里](https://appktavsiei5995.pc.xiaoe-tech.com/detail/i_6298793ae4b09dda12708be8/1) 查看:
83+
84+
![](https://labuladong.github.io/algo/images/qrcode.jpg)

动态规划系列/动态规划之KMP字符匹配算法.md

Lines changed: 56 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
11
# 动态规划之KMP字符匹配算法
22

3-
43
<p align='center'>
54
<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>
5+
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
66
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%[email protected]?style=flat-square&logo=Zhihu"></a>
7-
<a href="https://i.loli.net/2020/10/10/MhRTyUKfXZOlQYN.jpg"><img src="https://img.shields.io/badge/公众号[email protected]?style=flat-square&logo=WeChat"></a>
87
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站[email protected]?style=flat-square&logo=Bilibili"></a>
98
</p>
109

11-
![](../pictures/souyisou.png)
10+
![](https://labuladong.github.io/algo/images/souyisou1.png)
1211

13-
**《labuladong 的算法秘籍》、《labuladong 的刷题笔记》两本 PDF 和刷题插件 2.0 免费开放下载,详情见 [labuladong 的刷题三件套正式发布](https://mp.weixin.qq.com/s/yN4cHQRsFa5SWlacopHXYQ)**~
12+
**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,[第 11 期刷题打卡挑战(9/19 开始)](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 开始报名。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
1413

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

17-
[28.实现 strStr()](https://leetcode-cn.com/problems/implement-strstr)
15+
16+
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
17+
18+
| LeetCode | 力扣 | 难度 |
19+
| :----: | :----: | :----: |
20+
| [28. Implement strStr()](https://leetcode.com/problems/implement-strstr/) | [28. 实现 strStr()](https://leetcode.cn/problems/implement-strstr/) | 🟢
1821

1922
**-----------**
2023

24+
> 阅读本文之前,建议你先学习一下另一种字符串匹配算法:[Rabin Karp 字符匹配算法](https://labuladong.github.io/article/fname.html?fname=rabinkarp)
25+
2126
KMP 算法(Knuth-Morris-Pratt 算法)是一个著名的字符串匹配算法,效率很高,但是确实有点复杂。
2227

2328
很多读者抱怨 KMP 算法无法理解,这很正常,想到大学教材上关于 KMP 算法的讲解,也不知道有多少未来的 Knuth、Morris、Pratt 被提前劝退了。有一些优秀的同学通过手推 KMP 算法的过程来辅助理解该算法,这是一种办法,不过本文要从逻辑层面帮助读者理解算法的原理。十行代码之间,KMP 灰飞烟灭。
@@ -28,13 +33,13 @@ KMP 算法(Knuth-Morris-Pratt 算法)是一个著名的字符串匹配算法
2833

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

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

3540
首先还是简单介绍一下 KMP 算法和暴力匹配算法的不同在哪里,难点在哪里,和动态规划有啥关系。
3641

37-
暴力的字符串匹配算法很容易写,看一下它的运行逻辑:
42+
力扣第 28 题「实现 strStr」就是字符串匹配问题,暴力的字符串匹配算法很容易写,看一下它的运行逻辑:
3843

3944
```java
4045
// 暴力匹配(伪码)
@@ -57,19 +62,19 @@ int search(String pat, String txt) {
5762

5863
对于暴力算法,如果出现不匹配字符,同时回退 `txt``pat` 的指针,嵌套 for 循环,时间复杂度 `O(MN)`,空间复杂度`O(1)`。最主要的问题是,如果字符串中重复的字符比较多,该算法就显得很蠢。
5964

60-
比如 txt = "aaacaaab" pat = "aaab":
65+
比如 `txt = "aaacaaab", pat = "aaab"`
6166

62-
![brutal](../pictures/kmp/1.gif)
67+
![](https://labuladong.github.io/algo/images/kmp/1.gif)
6368

6469
很明显,`pat` 中根本没有字符 c,根本没必要回退指针 `i`,暴力解法明显多做了很多不必要的操作。
6570

6671
KMP 算法的不同之处在于,它会花费空间来记录一些信息,在上述情况中就会显得很聪明:
6772

68-
![kmp1](../pictures/kmp/2.gif)
73+
![](https://labuladong.github.io/algo/images/kmp/2.gif)
6974

70-
再比如类似的 txt = "aaaaaaab" pat = "aaab",暴力解法还会和上面那个例子一样蠢蠢地回退指针 `i`,而 KMP 算法又会耍聪明:
75+
再比如类似的 `txt = "aaaaaaab", pat = "aaab"`,暴力解法还会和上面那个例子一样蠢蠢地回退指针 `i`,而 KMP 算法又会耍聪明:
7176

72-
![kmp2](../pictures/kmp/3.gif)
77+
![](https://labuladong.github.io/algo/images/kmp/3.gif)
7378

7479
因为 KMP 算法知道字符 b 之前的字符 a 都是匹配的,所以每次只需要比较字符 b 是否被匹配就行了。
7580

@@ -92,21 +97,21 @@ pat = "aaab"
9297

9398
只不过对于 `txt1` 的下面这个即将出现的未匹配情况:
9499

95-
![](../pictures/kmp/txt1.jpg)
100+
![](https://labuladong.github.io/algo/images/kmp/txt1.jpg)
96101

97102
`dp` 数组指示 `pat` 这样移动:
98103

99-
![](../pictures/kmp/txt2.jpg)
104+
![](https://labuladong.github.io/algo/images/kmp/txt2.jpg)
100105

101-
PS:这个`j` 不要理解为索引,它的含义更准确地说应该是**状态**(state),所以它会出现这个奇怪的位置,后文会详述。
106+
> PS:这个`j` 不要理解为索引,它的含义更准确地说应该是**状态**(state),所以它会出现这个奇怪的位置,后文会详述。
102107
103108
而对于 `txt2` 的下面这个即将出现的未匹配情况:
104109

105-
![](../pictures/kmp/txt3.jpg)
110+
![](https://labuladong.github.io/algo/images/kmp/txt3.jpg)
106111

107112
`dp` 数组指示 `pat` 这样移动:
108113

109-
![](../pictures/kmp/txt4.jpg)
114+
![](https://labuladong.github.io/algo/images/kmp/txt4.jpg)
110115

111116
明白了 `dp` 数组只和 `pat` 有关,那么我们这样设计 KMP 算法就会比较漂亮:
112117

@@ -140,46 +145,45 @@ int pos2 = kmp.search("aaaaaaab"); //4
140145

141146
为什么说 KMP 算法和状态机有关呢?是这样的,我们可以认为 `pat` 的匹配就是状态的转移。比如当 pat = "ABABC":
142147

143-
![](../pictures/kmp/state.jpg)
148+
![](https://labuladong.github.io/algo/images/kmp/state.jpg)
144149

145150
如上图,圆圈内的数字就是状态,状态 0 是起始状态,状态 5(`pat.length`)是终止状态。开始匹配时 `pat` 处于起始状态,一旦转移到终止状态,就说明在 `txt` 中找到了 `pat`。比如说当前处于状态 2,就说明字符 "AB" 被匹配:
146151

147-
![](../pictures/kmp/state2.jpg)
152+
![](https://labuladong.github.io/algo/images/kmp/state2.jpg)
148153

149154
另外,处于不同状态时,`pat` 状态转移的行为也不同。比如说假设现在匹配到了状态 4,如果遇到字符 A 就应该转移到状态 3,遇到字符 C 就应该转移到状态 5,如果遇到字符 B 就应该转移到状态 0:
150155

151-
![](../pictures/kmp/state4.jpg)
156+
![](https://labuladong.github.io/algo/images/kmp/state4.jpg)
152157

153158
具体什么意思呢,我们来一个个举例看看。用变量 `j` 表示指向当前状态的指针,当前 `pat` 匹配到了状态 4:
154159

155-
![](../pictures/kmp/exp1.jpg)
160+
![](https://labuladong.github.io/algo/images/kmp/exp1.jpg)
156161

157162
如果遇到了字符 "A",根据箭头指示,转移到状态 3 是最聪明的:
158163

159-
![](../pictures/kmp/exp3.jpg)
164+
![](https://labuladong.github.io/algo/images/kmp/exp3.jpg)
160165

161166
如果遇到了字符 "B",根据箭头指示,只能转移到状态 0(一夜回到解放前):
162167

163-
![](../pictures/kmp/exp5.jpg)
168+
![](https://labuladong.github.io/algo/images/kmp/exp5.jpg)
164169

165170
如果遇到了字符 "C",根据箭头指示,应该转移到终止状态 5,这也就意味着匹配完成:
166171

167-
![](../pictures/kmp/exp7.jpg)
168-
172+
![](https://labuladong.github.io/algo/images/kmp/exp7.jpg)
169173

170174
当然了,还可能遇到其他字符,比如 Z,但是显然应该转移到起始状态 0,因为 `pat` 中根本都没有字符 Z:
171175

172-
![](../pictures/kmp/z.jpg)
176+
![](https://labuladong.github.io/algo/images/kmp/z.jpg)
173177

174178
这里为了清晰起见,我们画状态图时就把其他字符转移到状态 0 的箭头省略,只画 `pat` 中出现的字符的状态转移:
175179

176-
![](../pictures/kmp/allstate.jpg)
180+
![](https://labuladong.github.io/algo/images/kmp/allstate.jpg)
177181

178182
KMP 算法最关键的步骤就是构造这个状态转移图。**要确定状态转移的行为,得明确两个变量,一个是当前的匹配状态,另一个是遇到的字符**;确定了这两个变量后,就可以知道这个情况下应该转移到哪个状态。
179183

180184
下面看一下 KMP 算法根据这幅状态转移图匹配字符串 `txt` 的过程:
181185

182-
![](../pictures/kmp/kmp.gif)
186+
![](https://labuladong.github.io/algo/images/kmp/kmp.gif)
183187

184188
**请记住这个 GIF 的匹配过程,这就是 KMP 算法的核心逻辑**
185189

@@ -234,29 +238,29 @@ for 0 <= j < M: # 状态
234238

235239
这个 next 状态应该怎么求呢?显然,**如果遇到的字符 `c``pat[j]` 匹配的话**,状态就应该向前推进一个,也就是说 `next = j + 1`,我们不妨称这种情况为**状态推进**
236240

237-
![](../pictures/kmp/forward.jpg)
241+
![](https://labuladong.github.io/algo/images/kmp/forward.jpg)
238242

239243
**如果字符 `c``pat[j]` 不匹配的话**,状态就要回退(或者原地不动),我们不妨称这种情况为**状态重启**
240244

241-
![](../pictures/kmp/back.jpg)
245+
![](https://labuladong.github.io/algo/images/kmp/back.jpg)
242246

243247
那么,如何得知在哪个状态重启呢?解答这个问题之前,我们再定义一个名字:**影子状态**(我编的名字),用变量 `X` 表示。**所谓影子状态,就是和当前状态具有相同的前缀**。比如下面这种情况:
244248

245-
![](../pictures/kmp/shadow.jpg)
249+
![](https://labuladong.github.io/algo/images/kmp/shadow.jpg)
246250

247251
当前状态 `j = 4`,其影子状态为 `X = 2`,它们都有相同的前缀 "AB"。因为状态 `X` 和状态 `j` 存在相同的前缀,所以当状态 `j` 准备进行状态重启的时候(遇到的字符 `c``pat[j]` 不匹配),可以通过 `X` 的状态转移图来获得**最近的重启位置**
248252

249253
比如说刚才的情况,如果状态 `j` 遇到一个字符 "A",应该转移到哪里呢?首先只有遇到 "C" 才能推进状态,遇到 "A" 显然只能进行状态重启。**状态 `j` 会把这个字符委托给状态 `X` 处理,也就是 `dp[j]['A'] = dp[X]['A']`**
250254

251-
![](../pictures/kmp/shadow1.jpg)
255+
![](https://labuladong.github.io/algo/images/kmp/shadow1.jpg)
252256

253257
为什么这样可以呢?因为:既然 `j` 这边已经确定字符 "A" 无法推进状态,**只能回退**,而且 KMP 就是要**尽可能少的回退**,以免多余的计算。那么 `j` 就可以去问问和自己具有相同前缀的 `X`,如果 `X` 遇见 "A" 可以进行「状态推进」,那就转移过去,因为这样回退最少。
254258

255-
![](../pictures/kmp/A.gif)
259+
![](https://labuladong.github.io/algo/images/kmp/A.gif)
256260

257261
当然,如果遇到的字符是 "B",状态 `X` 也不能进行「状态推进」,只能回退,`j` 只要跟着 `X` 指引的方向回退就行了:
258262

259-
![](../pictures/kmp/shadow2.jpg)
263+
![](https://labuladong.github.io/algo/images/kmp/shadow2.jpg)
260264

261265
你也许会问,这个 `X` 怎么知道遇到字符 "B" 要回退到状态 0 呢?因为 `X` 永远跟在 `j` 的身后,状态 `X` 如何转移,在之前就已经算出来了。动态规划算法不就是利用过去的结果解决现在的问题吗?
262266

@@ -350,7 +354,7 @@ for (int i = 0; i < N; i++) {
350354

351355
下面来看一下状态转移图的完整构造过程,你就能理解状态 `X` 作用之精妙了:
352356

353-
![](../pictures/kmp/dfa.gif)
357+
![](https://labuladong.github.io/algo/images/kmp/dfa.gif)
354358

355359
至此,KMP 算法的核心终于写完啦啦啦啦!看下 KMP 算法的完整代码吧:
356360

@@ -419,15 +423,26 @@ KMP 算法也就是动态规划那点事,我们的公众号文章目录有一
419423

420424

421425

426+
<hr>
427+
<details>
428+
<summary><strong>引用本文的文章</strong></summary>
429+
430+
- [我的刷题心得](https://labuladong.github.io/article/fname.html?fname=算法心得)
431+
- [滑动窗口算法延伸:Rabin Karp 字符匹配算法](https://labuladong.github.io/article/fname.html?fname=rabinkarp)
432+
433+
</details><hr>
434+
435+
436+
437+
438+
422439
**_____________**
423440

424-
**刷算法,学套路,认准 labuladong,公众号和 [在线电子书](https://labuladong.gitee.io/algo/) 持续更新最新文章**
441+
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「进群」可加入算法群;回复「PDF」可获取精华文章 PDF**
442+
443+
![](https://labuladong.github.io/algo/images/souyisou2.png)
425444

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

428-
<p align='center'>
429-
<img src="../pictures/qrcode.jpg" width=200 >
430-
</p>
431446
======其他语言代码======
432447

433448
[28.实现 strStr()](https://leetcode-cn.com/problems/implement-strstr)

0 commit comments

Comments
 (0)