diff --git a/notes/Leetcode 题解 - 搜索.md b/notes/Leetcode 题解 - 搜索.md
index 20496896..29600e69 100644
--- a/notes/Leetcode 题解 - 搜索.md
+++ b/notes/Leetcode 题解 - 搜索.md
@@ -27,6 +27,8 @@
* [13. 分割字符串使得每个部分都是回文数](#13-分割字符串使得每个部分都是回文数)
* [14. 数独](#14-数独)
* [15. N 皇后](#15-n-皇后)
+ * [附录:实测](#附录实测)
+ * [A1. 岛屿数量的递归深度](#a1-岛屿数量的递归深度)
@@ -54,13 +56,15 @@
- 4 -\> {}
- 3 -\> {}
-每一层遍历的节点都与根节点距离相同。设 di 表示第 i 个节点与根节点的距离,推导出一个结论:对于先遍历的节点 i 与后遍历的节点 j,有 di <= dj。利用这个结论,可以求解最短路径等 **最优解** 问题:第一次遍历到目的节点,其所经过的路径为最短路径。应该注意的是,使用 BFS 只能求解无权图的最短路径,无权图是指从一个节点到另一个节点的代价都记为 1。
+每一层遍历的节点都与根节点距离相同。设 di 表示第 i 个节点与根节点的距离,推导出一个结论:对于先遍历的节点 i 与后遍历的节点 j,有 di <= dj。利用这个结论,可以求解最短路径等最优解问题:第一次遍历到目的节点,其所经过的路径为最短路径。需要注意的是,BFS 只能求解无权图的最短路径,无权图是指从一个节点到另一个节点的代价都记为 1。
在程序实现 BFS 时需要考虑以下问题:
- 队列:用来存储每一轮遍历得到的节点;
- 标记:对于遍历过的节点,应该将它标记,防止重复遍历。
+下面的代码每轮先记下队列大小 size,取出的这 size 个节点正好是一层。
+
### 1. 计算在网格中从原点到特定点的最短路径长度
1091\. Shortest Path in Binary Matrix (Medium)
@@ -68,13 +72,13 @@
[Leetcode](https://leetcode.com/problems/shortest-path-in-binary-matrix/) / [力扣](https://leetcode-cn.com/problems/shortest-path-in-binary-matrix/)
```text
-[[1,1,0,1],
- [1,0,1,0],
- [1,1,1,1],
- [1,0,1,1]]
+Input: grid = [[0,0,0],
+ [1,1,0],
+ [1,1,0]]
+Output: 4
```
-题目描述:0 表示可以经过某个位置,求解从左上角到右下角的最短路径长度。
+题目描述:0 表示可以经过,每一步可以走到相邻的 8 个格子(包括斜对角),求从左上角到右下角最少经过几个格子,无法到达时返回 -1。
```java
public int shortestPathBinaryMatrix(int[][] grids) {
@@ -208,6 +212,8 @@ Explanation: The endWord "cog" is not in wordList, therefore no possible transfo
题目描述:找出一条从 beginWord 到 endWord 的最短路径,每次移动规定为改变一个字符,并且改变之后的字符串必须在 wordList 中。
+把每个单词看成节点,只差一个字符的两个单词之间连一条边,问题就变成了无权图的最短路径。
+
```java
public int ladderLength(String beginWord, String endWord, List wordList) {
wordList.add(beginWord);
@@ -283,11 +289,11 @@ private int getShortestPath(List[] graphic, int start, int end) {
而深度优先搜索在得到一个新节点时立即对新节点进行遍历:从节点 0 出发开始遍历,得到新节点 6 时,立马对新节点 6 进行遍历,得到新节点 4;如此反复以这种方式遍历新节点,直到没有新节点了,此时返回。返回到根节点 0 的情况是,继续对根节点 0 进行遍历,得到新节点 2,然后继续以上步骤。
-从一个节点出发,使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种 **可达性** 问题。
+从一个节点出发,使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种可达性问题。
在程序实现 DFS 时需要考虑以下问题:
-- 栈:用栈来保存当前节点信息,当遍历新节点返回时能够继续遍历当前节点。可以使用递归栈。
+- 栈:用栈来保存当前节点信息,当遍历新节点返回时能够继续遍历当前节点,通常直接使用递归栈;
- 标记:和 BFS 一样同样需要对已经遍历过的节点进行标记。
### 1. 查找最大的连通面积
@@ -307,6 +313,8 @@ private int getShortestPath(List[] graphic, int start, int end) {
[0,0,0,0,0,0,0,1,1,0,0,0,0]]
```
+题目描述:求上下左右相连的 1 组成的最大区域有多少个格子,上面的例子为 6。
+
```java
private int m, n;
private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
@@ -355,7 +363,7 @@ Input:
Output: 3
```
-可以将矩阵表示看成一张无向图。
+可以将矩阵看成一张无向图,每个岛屿是一个连通分量,每次 DFS 把一整座岛屿改为 0,避免重复计数。
```java
private int m, n;
@@ -390,6 +398,27 @@ private void dfs(char[][] grid, int i, int j) {
}
```
+需要注意的是,递归深度最多等于岛屿的格子数,300 × 300 的网格全是 1 时深达 9 万层,JDK 21 默认的线程栈会溢出(见[附录 A1](#a1-岛屿数量的递归深度)),这时可以把 dfs 换成下面的 BFS:
+
+```java
+private void bfs(char[][] grid, int i, int j) {
+ Queue queue = new LinkedList<>();
+ queue.add(new int[]{i, j});
+ grid[i][j] = '0';
+ while (!queue.isEmpty()) {
+ int[] cur = queue.poll();
+ for (int[] d : direction) {
+ int r = cur[0] + d[0], c = cur[1] + d[1];
+ if (r < 0 || r >= m || c < 0 || c >= n || grid[r][c] == '0') {
+ continue;
+ }
+ grid[r][c] = '0';
+ queue.add(new int[]{r, c});
+ }
+ }
+}
+```
+
### 3. 好友关系的连通分量数目
547\. Number of Provinces (Medium)
@@ -460,6 +489,8 @@ X O X X
先填充最外侧,剩下的就是里侧了。
+
+
```java
private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
private int m, n;
@@ -524,7 +555,11 @@ Return:
[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (positions with parentheses in above matrix).
```
-左边和上边是太平洋,右边和下边是大西洋,内部的数字代表海拔,海拔高的地方的水能够流到低的地方,求解水能够流到太平洋和大西洋的所有位置。
+左边和上边是太平洋,右边和下边是大西洋,内部的数字代表海拔,水能从一个格子流向海拔不高于它的相邻格子,求解水能够流到太平洋和大西洋的所有位置。
+
+和上一题一样从边界出发:分别从两个大洋的沿岸逆着水流搜索,两边都能走到的格子就是答案。
+
+
```java
private int m, n;
@@ -585,14 +620,30 @@ private void dfs(int r, int c, boolean[][] canReach) {
Backtracking(回溯)属于 DFS。
-- 普通 DFS 主要用在 **可达性问题** ,这种问题只需要执行到特定的位置然后返回即可。
-- 而 Backtracking 主要用于求解 **排列组合** 问题,例如有 { 'a','b','c' } 三个字符,求解所有由这三个字符排列得到的字符串,这种问题在执行到特定的位置返回之后还会继续执行求解过程。
+- 普通 DFS 主要用在可达性问题,这种问题只需要执行到特定的位置然后返回即可;
+- 而 Backtracking 主要用于求解排列组合问题,例如有 { 'a','b','c' } 三个字符,求解所有由这三个字符排列得到的字符串,这种问题在执行到特定的位置返回之后还会继续执行求解过程。
+
+各题的代码都是下面的结构,加入结果时要复制当前选择,因为这个列表之后还会被修改:
+
+```text
+backtracking(当前选择):
+ if 满足结束条件:
+ 结果.add(复制当前选择)
+ return
+ for 候选 in 这一层的候选:
+ if 不合法: continue
+ 做选择(加入列表、标记已访问)
+ backtracking(当前选择)
+ 撤销选择(移出列表、取消标记)
+```
因为 Backtracking 不是立即返回,而要继续求解,因此在程序实现时,需要注意对元素的标记问题:
- 在访问一个新元素进入新的递归调用时,需要将新元素标记为已经访问,这样才能在继续递归调用时不用重复访问该元素;
- 但是在递归返回时,需要将元素标记为未访问,因为只需要保证在一个递归链中不同时访问一个元素,可以访问已经访问过但是不在当前递归链中的元素。
+[岛屿数量](#2-矩阵中的连通分量数目)的标记表示格子已经处理过,不再撤销;[在矩阵中寻找字符串](#3-在矩阵中寻找字符串)的标记表示格子在当前路径上,换一条路径时要撤销。
+
### 1. 数字键盘组合
17\. Letter Combinations of a Phone Number (Medium)
@@ -644,6 +695,8 @@ Given "25525511135",
return ["255.255.11.135", "255.255.111.35"].
```
+每一层截取 1~3 个字符作为一段,段的值不超过 255 且不能有前导 0;截满 4 段且正好用完字符时得到一个结果。
+
```java
public List restoreIpAddresses(String s) {
List addresses = new ArrayList<>();
@@ -695,6 +748,8 @@ word = "SEE", -> returns true,
word = "ABCB", -> returns false.
```
+同一个格子在一条路径中只能用一次,例如 "ABCB" 走到 C 之后,相邻的 B 已经在路径上,因此返回 false。
+
```java
private final static int[][] direction = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private int m;
@@ -766,7 +821,6 @@ private boolean backtracking(int curLen, int r, int c, boolean[][] visited, fina
```
```java
-
public List binaryTreePaths(TreeNode root) {
List paths = new ArrayList<>();
if (root == null) {
@@ -825,6 +879,10 @@ private String buildPath(List values) {
]
```
+下图是 [1,2,3] 的前 12 步:
+
+
+
```java
public List> permute(int[] nums) {
List> permutes = new ArrayList<>();
@@ -865,7 +923,11 @@ private void backtracking(List permuteList, List> permute
数组元素可能含有相同的元素,进行排列时就有可能出现重复的排列,要求重复的排列只返回一个。
-在实现上,和 Permutations 不同的是要先排序,然后在添加一个元素时,判断这个元素是否等于前一个元素,如果等于,并且前一个元素还未访问,那么就跳过这个元素。
+和 Permutations 不同的是要先排序,让相同的元素相邻;添加一个元素时,如果它等于前一个元素,并且前一个元素还未访问,就跳过这个元素。
+
+这样值相同的元素只能按下标顺序进入路径,每个排列只生成一次:
+
+
```java
public List> permuteUnique(int[] nums) {
@@ -917,6 +979,8 @@ If n = 4 and k = 2, a solution is:
]
```
+组合不考虑顺序,所以每一层只从比上一个数大的数中选,选出的组合递增,不会重复。
+
```java
public List> combine(int n, int k) {
List> combinations = new ArrayList<>();
@@ -930,7 +994,7 @@ private void backtracking(List combineList, List> combina
combinations.add(new ArrayList<>(combineList));
return;
}
- for (int i = start; i <= n - k + 1; i++) { // 剪枝
+ for (int i = start; i <= n - k + 1; i++) { // 剪枝:后面至少还要剩下 k - 1 个数
combineList.add(i);
backtracking(combineList, combinations, i + 1, k - 1, n);
combineList.remove(combineList.size() - 1);
@@ -950,6 +1014,8 @@ A solution set is:
[[7],[2, 2, 3]]
```
+每个数可以重复选取,所以递归时传入 i 而不是 i + 1。
+
```java
public List> combinationSum(int[] candidates, int target) {
List> combinations = new ArrayList<>();
@@ -991,6 +1057,8 @@ A solution set is:
]
```
+每个数只能用一次,递归时传入 i + 1;数组中有重复的数,去重方法与 [含有相同元素求排列](#6-含有相同元素求排列) 相同。
+
```java
public List> combinationSum2(int[] candidates, int target) {
List> combinations = new ArrayList<>();
@@ -1035,7 +1103,7 @@ Output:
[[1,2,6], [1,3,5], [2,3,4]]
```
-从 1-9 数字中选出 k 个数不重复的数,使得它们的和为 n。
+从 1~9 中选出 k 个不重复的数,使得它们的和为 n,写法与 [组合](#7-组合) 相同。
```java
public List> combinationSum3(int k, int n) {
@@ -1069,7 +1137,9 @@ private void backtracking(int k, int n, int start,
[Leetcode](https://leetcode.com/problems/subsets/description/) / [力扣](https://leetcode-cn.com/problems/subsets/description/)
-找出集合的所有子集,子集不能重复,[1, 2] 和 [2, 1] 这种子集算重复
+找出集合的所有子集,子集不能重复,[1, 2] 和 [2, 1] 这种子集算重复。
+
+子集就是大小为 0~n 的所有组合,对每种大小各做一次组合的回溯。
```java
public List> subsets(int[] nums) {
@@ -1116,6 +1186,8 @@ If nums = [1,2,2], a solution is:
]
```
+去重方法与 [含有相同元素求排列](#6-含有相同元素求排列) 相同。
+
```java
public List> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
@@ -1164,6 +1236,8 @@ Return
]
```
+每一层从剩余字符串的开头切出一段回文串,再对剩下的部分递归。
+
```java
public List> partition(String s) {
List> partitions = new ArrayList<>();
@@ -1204,6 +1278,8 @@ private boolean isPalindrome(String s, int begin, int end) {
+用三个标记数组记录每一行、每一列、每一个 3 \* 3 宫中已经出现的数字。找到一个解就返回 true,不再撤销,棋盘上留下的就是答案。
+
```java
private boolean[][] rowsUsed = new boolean[9][10];
private boolean[][] colsUsed = new boolean[9][10];
@@ -1212,7 +1288,7 @@ private char[][] board;
public void solveSudoku(char[][] board) {
this.board = board;
- for (int i = 0; i < 9; i++)
+ for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] == '.') {
continue;
@@ -1222,7 +1298,8 @@ public void solveSudoku(char[][] board) {
colsUsed[j][num] = true;
cubesUsed[cubeNum(i, j)][num] = true;
}
- backtracking(0, 0);
+ }
+ backtracking(0, 0);
}
private boolean backtracking(int row, int col) {
@@ -1322,3 +1399,49 @@ private void backtracking(int row) {
}
}
```
+
+## 附录:实测
+
+### A1. 岛屿数量的递归深度
+
+环境:JDK 21.0.13(Temurin,macOS arm64,默认 ThreadStackSize = 2048 KB)。全为 1 的 N × N 网格,用与正文相同的 dfs(方向顺序右、左、下、上,会蛇形走完整个网格)并统计最大递归深度,命令为 `java Deep200 N`:
+
+```java
+static int depth, maxDepth;
+
+static void dfs(char[][] g, int i, int j) {
+ if (i < 0 || i >= m || j < 0 || j >= n || g[i][j] == '0') return;
+ depth++;
+ maxDepth = Math.max(maxDepth, depth);
+ g[i][j] = '0';
+ for (int[] d : direction) dfs(g, i + d[0], j + d[1]);
+ depth--;
+}
+
+// main 中:全 1 的 N × N 网格
+try {
+ dfs(grid, 0, 0);
+ System.out.println(N + "x" + N + " all 1: max recursion depth " + maxDepth);
+} catch (StackOverflowError e) {
+ System.out.println(N + "x" + N + " all 1: StackOverflowError at depth " + depth);
+}
+```
+
+```text
+# 第一次运行
+100x100 all 1: max recursion depth 10000
+200x200 all 1: StackOverflowError at depth 26231
+300x300 all 1: StackOverflowError at depth 25541
+# 第二次运行
+100x100 all 1: max recursion depth 10000
+200x200 all 1: StackOverflowError at depth 21931
+300x300 all 1: StackOverflowError at depth 14533
+```
+
+正文的 numIslands 在 200 × 200 和 300 × 300 时同样抛出 StackOverflowError;换成 BFS 后三种规模都返回 1。溢出的深度与栈帧大小、JIT 编译状态有关,每次运行不同。
+
+## 参考资料
+
+- [LeetCode 题库](https://leetcode.com/problemset/)
+- Sedgewick R, Wayne K. 算法(第 4 版)[M]. 人民邮电出版社, 2012.
+- [Algorithms, 4th Edition](https://algs4.cs.princeton.edu/home/)
diff --git a/notes/pics/search-n-queens-diag135.png b/notes/pics/search-n-queens-diag135.png
index bb4a36b7..ab06b670 100644
Binary files a/notes/pics/search-n-queens-diag135.png and b/notes/pics/search-n-queens-diag135.png differ
diff --git a/notes/pics/search-n-queens-diag45.png b/notes/pics/search-n-queens-diag45.png
index fe3aaf5d..5e8c2bd6 100644
Binary files a/notes/pics/search-n-queens-diag45.png and b/notes/pics/search-n-queens-diag45.png differ
diff --git a/notes/pics/search-pacific-atlantic.png b/notes/pics/search-pacific-atlantic.png
new file mode 100644
index 00000000..6fb75d3e
Binary files /dev/null and b/notes/pics/search-pacific-atlantic.png differ
diff --git a/notes/pics/search-perm-dedup.png b/notes/pics/search-perm-dedup.png
new file mode 100644
index 00000000..1fc29e3b
Binary files /dev/null and b/notes/pics/search-perm-dedup.png differ
diff --git a/notes/pics/search-permute-trace.png b/notes/pics/search-permute-trace.png
new file mode 100644
index 00000000..6cd52566
Binary files /dev/null and b/notes/pics/search-permute-trace.png differ
diff --git a/notes/pics/search-surrounded-regions.png b/notes/pics/search-surrounded-regions.png
new file mode 100644
index 00000000..eadbe8c0
Binary files /dev/null and b/notes/pics/search-surrounded-regions.png differ