Skip to content

Commit 1deca48

Browse files
committed
修正文章内容
1 parent b316af0 commit 1deca48

File tree

9 files changed

+152
-42
lines changed

9 files changed

+152
-42
lines changed

docs/01_array/01_15_array_two_pointers.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -380,8 +380,8 @@ class Solution:
380380

381381
##### 思路 1:复杂度分析
382382

383-
- **时间复杂度**:$O(n)$。
384-
- **空间复杂度**:$O(1)$。
383+
- **时间复杂度**:$O(m \log m + n \log n)$,其中 $m$ 和 $n$ 分别为两个数组的长度。排序用时 $O(m \log m + n \log n)$,双指针遍历用时 $O(m + n)$,因此总的时间复杂度为 $O(m \log m + n \log n)$。
384+
- **空间复杂度**:$O(\min(m, n))$。
385385

386386
## 5. 双指针总结
387387

docs/04_string/04_09_ac_automaton.md

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@
1212

1313
如果只使用字典树(Trie),虽然能够共享前缀,但每次匹配失败都必须回到根节点重新开始,无法实现高效跳转,最坏情况下复杂度也接近 $O(n \times m)$。
1414

15-
16-
17-
1815
### 1.2 AC 自动机高效匹配原理
1916

2017
AC 自动机能够高效解决多模式匹配问题,其核心思想是:将所有模式串构建为一棵字典树(Trie),并为每个节点设置失配指针(fail 指针),结合 KMP 算法的失配机制,实现对文本串的一次扫描即可同时匹配多个模式串。
@@ -65,9 +62,9 @@ AC 自动机的时间复杂度为 $O(n + m + k)$,其中 $n$ 为文本串长度
6562

6663
失配指针的构造遵循以下规则:
6764

68-
1. **根节点**:失配指针为 `null`
69-
2. **根节点的子节点**:失配指针都指向根节点
70-
3. **其他节点**:从父节点的失配指针开始查找,如果找到对应字符的子节点,则指向该子节点;否则继续向上查找,直到找到或到达根节点
65+
1. **根节点**:失配指针为 `null`
66+
2. **根节点的子节点**:失配指针都指向根节点
67+
3. **其他节点**:从父节点的失配指针开始查找,如果找到对应字符的子节点,则指向该子节点;否则继续向上查找,直到找到或到达根节点
7168

7269
#### 2.2.3 构造示例
7370

@@ -84,36 +81,37 @@ AC 自动机的时间复杂度为 $O(n + m + k)$,其中 $n$ 为文本串长度
8481
```
8582

8683
**失配指针构造过程**
84+
8785
- `s``root`(根节点子节点指向根节点)
8886
- `h``root`(根节点子节点指向根节点)
8987
- `sa``root`(根节点没有 `a` 子节点)
9088
- `sh``h`(根节点有 `h` 子节点)
91-
- `he``e`(根节点有 `e` 子节点)
92-
- `hr``root`(根节点没有 `r` 子节点)
9389
- `say``root`(根节点没有 `y` 子节点)
9490
- `she``he``h` 节点有 `e` 子节点)
9591
- `shr``root``h` 和根节点都没有 `r` 子节点)
96-
- `her``root``e` 和根节点都没有 `r` 子节点)
92+
- `he``root`(根节点没有 `e` 子节点)
93+
- `her``root``root` 没有 `r` 子节点)
9794

9895
#### 2.2.4 失配指针的作用
9996

10097
失配指针的主要作用是:
101-
1. **快速跳转**:匹配失败时,不需要回到根节点重新开始
102-
2. **避免重复比较**:利用已匹配的部分信息,避免重复比较
103-
3. **保证匹配连续性**:确保跳转后当前匹配的字符串仍是某个模式串的前缀
98+
99+
1. **快速跳转**:匹配失败时,不需要回到根节点重新开始。
100+
2. **避免重复比较**:利用已匹配的部分信息,避免重复比较。
101+
3. **保证匹配连续性**:确保跳转后当前匹配的字符串仍是某个模式串的前缀。
104102

105103
### 2.3 文本串匹配过程
106104

107105
有了字典树和失配指针,我们就可以进行高效的文本串匹配了。
108106

109107
#### 2.3.1 匹配算法流程
110108

111-
1. **初始化**:从根节点开始
109+
1. **初始化**:从根节点开始
112110
2. **字符匹配**:对于文本串中的每个字符:
113-
- 如果当前节点有对应字符的子节点,移动到该子节点
114-
- 否则,沿着失配指针向上查找,直到找到匹配的子节点或到达根节点
115-
3. **模式串检测**:每到达一个节点,检查该节点是否为某个模式串的结尾
116-
4. **输出匹配结果**:如果找到匹配的模式串,记录其位置和内容
111+
- 如果当前节点有对应字符的子节点,移动到该子节点
112+
- 否则,沿着失配指针向上查找,直到找到匹配的子节点或到达根节点
113+
3. **模式串检测**:每到达一个节点,检查该节点是否为某个模式串的结尾
114+
4. **输出匹配结果**:如果找到匹配的模式串,记录其位置和内容
117115

118116
#### 2.3.2 匹配过程示例
119117

@@ -125,12 +123,12 @@ AC 自动机的时间复杂度为 $O(n + m + k)$,其中 $n$ 为文本串长度
125123
| `a` | 根节点 | 无匹配,保持根节点 | - | - |
126124
| `s` | 根节点 | 移动到 `s` 节点 | `s` | - |
127125
| `h` | `s` 节点 | 移动到 `sh` 节点 | `sh` | - |
128-
| `e` | `sh` 节点 | 移动到 `she` 节点 | `she` | **找到 `she`** |
129-
| `r` | `she` 节点 | 失配,跳转到根节点 | - | - |
130-
| `h` | 根节点 | 移动到 `h` 节点 | `h` | - |
131-
| `s` | `h` 节点 | 失配,跳转到根节点,再移动到 `s` 节点 | `s` | - |
126+
| `e` | `sh` 节点 | 移动到 `she` 节点 | `she` | **找到 `she``he`**(沿 fail 链) |
127+
| `r` | `she` 节点 | 失配,沿 fail 跳到 `he`,再转移到 `her` 节点 | `her` | **找到 `her`** |
128+
| `h` | `her` 节点 | 失配,跳到根节点,再移动到 `h` 节点 | `h` | - |
129+
| `s` | `h` 节点 | 失配,跳到根节点,再移动到 `s` 节点 | `s` | - |
132130

133-
**最终结果**:在文本串 `yasherhs` 中找到模式串 `she`(位置 2-4)。
131+
**最终结果**:在文本串 `yasherhs` 中找到模式串 `she`(位置 3-5)、`he`(位置 4-5)、`her`(位置 4-6)。
134132

135133

136134
## 3. AC 自动机代码实现

docs/08_dynamic_programming/08_03_linear_dp_01.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,59 @@ class Solution:
114114
- **时间复杂度**:$O(n^2)$。外层和内层循环各遍历一次数组,整体为 $O(n^2)$,最后取最大值为 $O(n)$,因此总时间复杂度为 $O(n^2)$。
115115
- **空间复杂度**:$O(n)$。仅需一个长度为 $n$ 的一维数组存储状态,故空间复杂度为 $O(n)$。
116116

117+
##### 思路 2:进阶的线性 DP + 二分查找
118+
119+
###### 1. 算法思想
120+
121+
使用 **数组作为可实时更新内部的单调栈**,结合 **贪心法****二分查找** 来优化时间复杂度。
122+
123+
核心思想:
124+
- $tails[k]$ 表示长度为 $k+1$ 的 LIS 的最小末尾元素(无闲置索引,动态扩展)
125+
- 对于每个元素 $x$,使用二分查找在 $tails$ 中找到第一个 $\ge x$ 的位置
126+
- 如果 $x$ 比所有元素都大,则追加到 $tails$ 末尾,形成新的 LIS 长度
127+
- 否则,用较小的 $x$ 更新 $tails[k]$,优化同长度 LIS 的末尾元素(变得更小)
128+
129+
###### 2. 算法步骤
130+
131+
1. 初始化 $tails$ 为空数组。
132+
2. 遍历数组 $nums$ 中的每个元素 $x$:
133+
- 使用二分查找在 $tails$ 中找到第一个 $\ge x$ 的位置 $k$。
134+
- 如果 $k == len(tails)$,说明 $x$ 比所有元素都大,追加到 $tails$ 末尾。
135+
- 否则,用 $x$ 更新 $tails[k]$,优化同长度 LIS 的末尾元素。
136+
3. 返回 $tails$ 的长度,即为最终 LIS 的长度。
137+
138+
##### 思路 2:进阶的线性 DP + 二分查找代码
139+
140+
```python
141+
import bisect
142+
143+
class Solution:
144+
def lengthOfLIS(self, nums: List[int]) -> int:
145+
if len(nums) == 1:
146+
return 1
147+
148+
# tails[k] 表示长度为 k+1 的 LIS 的最小末尾元素(无闲置索引,动态扩展)
149+
tails = list()
150+
for x in nums:
151+
# 二分查找:在tails中找首个≥x的位置
152+
k = bisect.bisect_left(tails, x)
153+
# 如果 x 比所有元素都大,k 的结果会是当前数组长度(末尾位置索引 +1)
154+
# x 直接新增在 tails 数组末尾,形成新问题(LIS 长度 +1)
155+
if k == len(tails):
156+
tails.append(x)
157+
# 用较小的 x 更新较大的 tails[k],优化同长度 LIS 问题的末尾元素(变得更小)
158+
else:
159+
tails[k] = x
160+
161+
# tails 的长度即为最终 LIS 问题的长度
162+
return len(tails)
163+
```
164+
165+
##### 思路 2:复杂度分析
166+
167+
- **时间复杂度**:$O(n \log n)$。遍历数组的时间复杂度是 $O(n)$,每个元素进行二分查找的时间复杂度是 $O(\log n)$,所以总体时间复杂度为 $O(n \log n)$。
168+
- **空间复杂度**:$O(n)$。$tails$ 数组最多存储 $n$ 个元素,所以总体空间复杂度为 $O(n)$。
169+
117170
### 3.2 经典例题:最大子数组和
118171

119172
在线性 DP 问题中,除了关注子序列相关的线性 DP,还常常遇到子数组相关的线性 DP 问题。
@@ -451,3 +504,4 @@ class Solution:
451504

452505
- 【书籍】算法竞赛进阶指南
453506
- 【文章】[动态规划概念和基础线性DP | 潮汐朝夕](https://chengzhaoxi.xyz/1a4a2483.html)
507+
- 【题解】[进阶的线性DP + 二分查找 | 来自评论区](https://github.com/xiaos2021)

docs/solutions/0001-0099/sort-colors.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,12 @@
4545
这道题我们也可以借鉴快速排序算法中的 $partition$ 过程,将 $1$ 作为基准数 $pivot$,然后将序列分为三部分:$0$(即比 $1$ 小的部分)、等于 $1$ 的部分、$2$(即比 $1$ 大的部分)。具体步骤如下:
4646

4747
1. 使用两个指针 $left$、$right$,分别指向数组的头尾。$left$ 表示当前处理好红色元素的尾部,$right$ 表示当前处理好蓝色的头部。
48-
2. 再使用一个下标 $index$ 遍历数组,如果遇到 $nums[index] == 0$,就交换 $nums[index]$ 和 $nums[left]$,同时将 $left$ 右移。如果遇到 $nums[index] == 2$,就交换 $nums[index]$ 和 $nums[right]$,同时将 $right$ 左移。
48+
2. 再使用一个下标 $index$ 遍历数组:
49+
- 如果遇到 $nums[index] == 0$,就交换 $nums[index]$ 和 $nums[left]$,同时将 $left$ 和 $index$ 都右移(因为交换后 $nums[index]$ 可能是从 $left$ 位置换过来的,需要继续检查)。
50+
- 如果遇到 $nums[index] == 2$,就交换 $nums[index]$ 和 $nums[right]$,同时将 $right$ 左移(注意 $index$ 不增加,因为交换后 $nums[index]$ 可能是从 $right$ 位置换过来的 0 或 1,需要再次检查)。
51+
- 如果遇到 $nums[index] == 1$,直接将 $index$ 右移。
4952
3. 直到 $index$ 移动到 $right$ 位置之后,停止遍历。遍历结束之后,此时 $left$ 左侧都是红色,$right$ 右侧都是蓝色。
5053

51-
注意:移动的时候需要判断 $index$ 和 $left$ 的位置,因为 $left$ 左侧是已经处理好的数组,所以需要判断 $index$ 的位置是否小于 $left$,小于的话,需要更新 $index$ 位置。
52-
5354
### 思路 1:代码
5455

5556
```python
@@ -59,11 +60,10 @@ class Solution:
5960
right = len(nums) - 1
6061
index = 0
6162
while index <= right:
62-
if index < left:
63-
index += 1
64-
elif nums[index] == 0:
63+
if nums[index] == 0:
6564
nums[index], nums[left] = nums[left], nums[index]
6665
left += 1
66+
index += 1
6767
elif nums[index] == 2:
6868
nums[index], nums[right] = nums[right], nums[index]
6969
right -= 1

docs/solutions/0300-0399/intersection-of-two-arrays.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,5 @@ class Solution:
100100

101101
### 思路 2:复杂度分析
102102

103-
- **时间复杂度**:$O(n)$。
104-
- **空间复杂度**:$O(1)$。
103+
- **时间复杂度**:$O(m \log m + n \log n)$,其中 $m$ 和 $n$ 分别为两个数组的长度。排序用时 $O(m \log m + n \log n)$,双指针遍历用时 $O(m + n)$,因此总的时间复杂度为 $O(m \log m + n \log n)$。
104+
- **空间复杂度**:$O(\min(m, n))$。

docs/solutions/0300-0399/longest-increasing-subsequence.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,59 @@ class Solution:
8989
- **时间复杂度**:$O(n^2)$。两重循环遍历的时间复杂度是 $O(n^2)$,最后求最大值的时间复杂度是 $O(n)$,所以总体时间复杂度为 $O(n^2)$。
9090
- **空间复杂度**:$O(n)$。用到了一维数组保存状态,所以总体空间复杂度为 $O(n)$。
9191

92+
### 思路 2:进阶的线性 DP + 二分查找
93+
94+
###### 1. 算法思想
95+
96+
使用 **数组作为可实时更新内部的单调栈**,结合 **贪心法****二分查找** 来优化时间复杂度。
97+
98+
核心思想:
99+
- $tails[k]$ 表示长度为 $k+1$ 的 LIS 的最小末尾元素(无闲置索引,动态扩展)
100+
- 对于每个元素 $x$,使用二分查找在 $tails$ 中找到第一个 $\ge x$ 的位置
101+
- 如果 $x$ 比所有元素都大,则追加到 $tails$ 末尾,形成新的 LIS 长度
102+
- 否则,用较小的 $x$ 更新 $tails[k]$,优化同长度 LIS 的末尾元素(变得更小)
103+
104+
###### 2. 算法步骤
105+
106+
1. 初始化 $tails$ 为空数组。
107+
2. 遍历数组 $nums$ 中的每个元素 $x$:
108+
- 使用二分查找在 $tails$ 中找到第一个 $\ge x$ 的位置 $k$。
109+
- 如果 $k == len(tails)$,说明 $x$ 比所有元素都大,追加到 $tails$ 末尾。
110+
- 否则,用 $x$ 更新 $tails[k]$,优化同长度 LIS 的末尾元素。
111+
3. 返回 $tails$ 的长度,即为最终 LIS 的长度。
112+
113+
### 思路 2:进阶的线性 DP + 二分查找代码
114+
115+
```python
116+
import bisect
117+
118+
class Solution:
119+
def lengthOfLIS(self, nums: List[int]) -> int:
120+
if len(nums) == 1:
121+
return 1
122+
123+
# tails[k] 表示长度为 k+1 的 LIS 的最小末尾元素(无闲置索引,动态扩展)
124+
tails = list()
125+
for x in nums:
126+
# 二分查找:在tails中找首个≥x的位置
127+
k = bisect.bisect_left(tails, x)
128+
# 如果 x 比所有元素都大,k 的结果会是当前数组长度(末尾位置索引 +1)
129+
# x 直接新增在 tails 数组末尾,形成新问题(LIS 长度 +1)
130+
if k == len(tails):
131+
tails.append(x)
132+
# 用较小的 x 更新较大的 tails[k],优化同长度 LIS 问题的末尾元素(变得更小)
133+
else:
134+
tails[k] = x
135+
136+
# tails 的长度即为最终 LIS 问题的长度
137+
return len(tails)
138+
```
139+
140+
### 思路 2:复杂度分析
141+
142+
- **时间复杂度**:$O(n \log n)$。遍历数组的时间复杂度是 $O(n)$,每个元素进行二分查找的时间复杂度是 $O(\log n)$,所以总体时间复杂度为 $O(n \log n)$。
143+
- **空间复杂度**:$O(n)$。$tails$ 数组最多存储 $n$ 个元素,所以总体空间复杂度为 $O(n)$。
144+
145+
## 参考资料
146+
147+
- 【题解】[进阶的线性DP + 二分查找 | 来自评论区](https://github.com/xiaos2021)

docs/solutions/0500-0599/number-of-provinces.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@
99

1010
## 题目大意
1111

12-
**描述**:有 `n` 个城市,其中一些彼此相连,另一些没有相连。如果城市 `a` 与城市 `b` 直接相连,且城市 `b` 与城市 `c` 直接相连,那么城市 `a` 与城市 `c` 间接相连。
12+
**描述**
13+
14+
`n` 个城市,其中一些彼此相连,另一些没有相连。如果城市 `a` 与城市 `b` 直接相连,且城市 `b` 与城市 `c` 直接相连,那么城市 `a` 与城市 `c` 间接相连。
1315

1416
「省份」是由一组直接或间接链接的城市组成,组内不含有其他没有相连的城市。
1517

1618
现在给定一个 `n * n` 的矩阵 `isConnected` 表示城市的链接关系。其中 `isConnected[i][j] = 1` 表示第 `i` 个城市和第 `j` 个城市直接相连,`isConnected[i][j] = 0` 表示第 `i` 个城市和第 `j` 个城市没有相连。
1719

18-
**要求**:根据给定的城市关系,返回「省份」的数量。
20+
**要求**
21+
22+
根据给定的城市关系,返回「省份」的数量。
1923

2024
**说明**
2125

@@ -81,15 +85,13 @@ class Solution:
8185
def findCircleNum(self, isConnected: List[List[int]]) -> int:
8286
size = len(isConnected)
8387
union_find = UnionFind(size)
88+
cnt = size
8489
for i in range(size):
8590
for j in range(i + 1, size):
8691
if isConnected[i][j] == 1:
87-
union_find.union(i, j)
88-
89-
res = set()
90-
for i in range(size):
91-
res.add(union_find.find(i))
92-
return len(res)
92+
if union_find.union(i, j):
93+
cnt -= 1
94+
return cnt
9395
```
9496

9597
### 思路 1:复杂度分析

docs/solutions/0600-0699/number-of-longest-increasing-subsequence.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
-`nums[j] < nums[i]`,而且 `dp[j] + 1 > dp[i]` 时,说明第一次找到 `dp[j] + 1`长度且以`nums[i]`结尾的最长递增子序列,则以 `nums[i]` 结尾的最长递增子序列的组合数就等于以 `nums[j]` 结尾的组合数,即 `count[i] = count[j]`
5454
-`nums[j] < nums[i]`,而且 `dp[j] + 1 == dp[i]` 时,说明以 `nums[i]` 结尾且长度为 `dp[j] + 1` 的递增序列已找到过一次了,则以 `nums[i]` 结尾的最长递增子序列的组合数要加上以 `nums[j]` 结尾的组合数,即 `count[i] += count[j]`
5555

56-
- 然后根据遍历 dp 数组得到的最长递增子序列的长度 max_length,然后再一次遍历 dp 数组,将所有 `dp[i] == max_length` 情况下的组合数 `coun[i]` 累加起来,即为最长递增序列的个数。
56+
- 然后根据遍历 dp 数组得到的最长递增子序列的长度 Z,然后再一次遍历 dp 数组,将所有 `dp[i] == max_length` 情况下的组合数 `coun[i]` 累加起来,即为最长递增序列的个数。
5757

5858
### 思路 1:动态规划代码
5959

docs/solutions/1200-1299/minimum-swaps-to-make-strings-equal.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
**说明**
2020

2121
- $1 \le s1.length, s2.length \le 1000$。
22-
- $s1$、$ s2$ 只包含 `'x'``'y'`
22+
- $s1$、$s2$ 只包含 `'x'``'y'`
2323

2424
**示例**
2525

0 commit comments

Comments
 (0)