diff --git a/chapter_appendix/contribution.md b/chapter_appendix/contribution.md index 8c260446b..35349488e 100644 --- a/chapter_appendix/contribution.md +++ b/chapter_appendix/contribution.md @@ -2,7 +2,7 @@ comments: true --- -# 12.2. 一起参与创作 +# 13.2. 一起参与创作 !!! success "开源的魅力" @@ -10,7 +10,7 @@ comments: true 由于作者能力有限,书中难免存在一些遗漏和错误,请您谅解。如果您发现了笔误、失效链接、内容缺失、文字歧义、解释不清晰或行文结构不合理等问题,请协助我们进行修正,以帮助其他读者获得更优质的学习资源。所有[撰稿人](https://github.com/krahets/hello-algo/graphs/contributors)将在仓库和网站主页上展示,以感谢他们对开源社区的无私奉献! -## 12.2.1. 内容微调 +## 13.2.1. 内容微调 在每个页面的右上角有一个「编辑」图标,您可以按照以下步骤修改文本或代码: @@ -24,7 +24,7 @@ comments: true 由于图片无法直接修改,因此需要通过新建 [Issue](https://github.com/krahets/hello-algo/issues) 或评论留言来描述图片问题,我们会尽快重新绘制并替换图片。 -## 12.2.2. 内容创作 +## 13.2.2. 内容创作 如果您有兴趣参与此开源项目,包括将代码翻译成其他编程语言、扩展文章内容等,那么需要实施 Pull Request 工作流程: @@ -34,7 +34,7 @@ comments: true 4. 将本地所做更改 Commit ,然后 Push 至远程仓库; 5. 刷新仓库网页,点击“Create pull request”按钮即可发起拉取请求; -## 12.2.3. Docker 部署 +## 13.2.3. Docker 部署 我们可以通过 Docker 来部署本项目。执行以下脚本,稍等片刻后,即可使用浏览器打开 `http://localhost:8000` 来访问本项目。 diff --git a/chapter_appendix/installation.md b/chapter_appendix/installation.md index afac25947..81c966726 100644 --- a/chapter_appendix/installation.md +++ b/chapter_appendix/installation.md @@ -2,49 +2,49 @@ comments: true --- -# 12.1. 编程环境安装 +# 13.1. 编程环境安装 -## 12.1.1. 安装 VSCode +## 13.1.1. 安装 VSCode 本书推荐使用开源轻量的 VSCode 作为本地 IDE ,下载并安装 [VSCode](https://code.visualstudio.com/) 。 -## 12.1.2. Java 环境 +## 13.1.2. Java 环境 1. 下载并安装 [OpenJDK](https://jdk.java.net/18/)(版本需满足 > JDK 9)。 2. 在 VSCode 的插件市场中搜索 `java` ,安装 Java Extension Pack 。 -## 12.1.3. C/C++ 环境 +## 13.1.3. C/C++ 环境 1. Windows 系统需要安装 [MinGW](https://sourceforge.net/projects/mingw-w64/files/)([配置教程](https://blog.csdn.net/qq_33698226/article/details/129031241)),MacOS 自带 Clang 无需安装。 2. 在 VSCode 的插件市场中搜索 `c++` ,安装 C/C++ Extension Pack 。 -## 12.1.4. Python 环境 +## 13.1.4. Python 环境 1. 下载并安装 [Miniconda3](https://docs.conda.io/en/latest/miniconda.html) 。 2. 在 VSCode 的插件市场中搜索 `python` ,安装 Python Extension Pack 。 -## 12.1.5. Go 环境 +## 13.1.5. Go 环境 1. 下载并安装 [go](https://go.dev/dl/) 。 2. 在 VSCode 的插件市场中搜索 `go` ,安装 Go 。 3. 快捷键 `Ctrl + Shift + P` 呼出命令栏,输入 go ,选择 `Go: Install/Update Tools` ,全部勾选并安装即可。 -## 12.1.6. JavaScript 环境 +## 13.1.6. JavaScript 环境 1. 下载并安装 [node.js](https://nodejs.org/en/) 。 2. 在 VSCode 的插件市场中搜索 `javascript` ,安装 JavaScript (ES6) code snippets 。 -## 12.1.7. C# 环境 +## 13.1.7. C# 环境 1. 下载并安装 [.Net 6.0](https://dotnet.microsoft.com/en-us/download) ; 2. 在 VSCode 的插件市场中搜索 `c#` ,安装 c# 。 -## 12.1.8. Swift 环境 +## 13.1.8. Swift 环境 1. 下载并安装 [Swift](https://www.swift.org/download/); 2. 在 VSCode 的插件市场中搜索 `swift` ,安装 [Swift for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=sswg.swift-lang)。 -## 12.1.9. Rust 环境 +## 13.1.9. Rust 环境 1. 下载并安装 [Rust](https://www.rust-lang.org/tools/install); 2. 在 VSCode 的插件市场中搜索 `rust` ,安装 [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)。 diff --git a/chapter_backtracking/backtracking_algorithm.md b/chapter_backtracking/backtracking_algorithm.md new file mode 100644 index 000000000..fdac5c56b --- /dev/null +++ b/chapter_backtracking/backtracking_algorithm.md @@ -0,0 +1,625 @@ +--- +comments: true +--- + +# 12.1. 回溯算法 + +「回溯算法 Backtracking Algorithm」是一种通过穷举来解决问题的方法,它的核心思想是从一个初始状态出发,暴力搜索所有可能的解决方案,当遇到正确的解则将其记录,直到找到解或者尝试了所有可能的选择都无法找到解为止。 + +回溯算法通常采用「深度优先搜索」来遍历解空间。在二叉树章节中,我们提到前序、中序和后序遍历都属于深度优先搜索。下面,我们从二叉树的前序遍历入手,逐步了解回溯算法的工作原理。 + +!!! question "例题一:在二叉树中搜索并返回所有值为 $7$ 的节点" + +**解题思路**:前序遍历这颗树,并判断当前节点的值是否为 $7$ ,若是则将该节点的值加入到结果列表 `res` 之中。 + +=== "Java" + + ```java title="preorder_find_nodes.java" + /* 前序遍历 */ + void preOrder(TreeNode root) { + if (root == null) { + return; + } + // 尝试 + if (root.val == 7) { + // 记录解 + res.add(root); + } + preOrder(root.left); + preOrder(root.right); + // 回退 + return; + } + ``` + +=== "C++" + + ```cpp title="preorder_find_nodes.cpp" + [class]{}-[func]{preOrder} + ``` + +=== "Python" + + ```python title="preorder_find_nodes.py" + def pre_order(root: TreeNode) -> None: + """前序遍历""" + if root is None: + return + if root.val == 7: + # 记录解 + res.append(root) + pre_order(root.left) + pre_order(root.right) + ``` + +=== "Go" + + ```go title="preorder_find_nodes.go" + [class]{}-[func]{preOrder} + ``` + +=== "JavaScript" + + ```javascript title="preorder_find_nodes.js" + [class]{}-[func]{preOrder} + ``` + +=== "TypeScript" + + ```typescript title="preorder_find_nodes.ts" + [class]{}-[func]{preOrder} + ``` + +=== "C" + + ```c title="preorder_find_nodes.c" + [class]{}-[func]{preOrder} + ``` + +=== "C#" + + ```csharp title="preorder_find_nodes.cs" + [class]{preorder_find_nodes}-[func]{preOrder} + ``` + +=== "Swift" + + ```swift title="preorder_find_nodes.swift" + [class]{}-[func]{preOrder} + ``` + +=== "Zig" + + ```zig title="preorder_find_nodes.zig" + [class]{}-[func]{preOrder} + ``` + + + +
Fig. 在前序遍历中搜索节点
+ +## 12.1.1. 尝试与回退 + +**之所以称之为回溯算法,是因为该算法在搜索解空间时会采用“尝试”与“回退”的策略**。当算法在搜索过程中遇到某个状态无法继续前进或无法得到满足条件的解时,它会撤销上一步的选择,退回到之前的状态,并尝试其他可能的选择。 + +对于例题一,访问每个节点都代表一次“尝试”,而越过叶结点或返回父节点的 `return` 则表示“回退”。 + +值得说明的是,**回退并不等价于函数返回**。为解释这一点,我们对例题一稍作拓展。 + +!!! question "在二叉树中搜索所有值为 $7$ 的节点,**返回根节点到这些节点的路径**" + +**解题思路**:在例题一代码的基础上,我们需要借助一个列表 `path` 记录访问过的节点路径。当访问到值为 $7$ 的节点时,则复制 `path` 并添加进结果列表 `res` 。遍历完成后,`res` 中保存的就是所有的解。 + +=== "Java" + + ```java title="preorder_find_paths.java" + /* 前序遍历 */ + void preOrder(TreeNode root) { + if (root == null) { + return; + } + // 尝试 + path.add(root); + if (root.val == 7) { + // 记录解 + res.add(new ArrayList<>(path)); + } + preOrder(root.left); + preOrder(root.right); + // 回退 + path.remove(path.size() - 1); + } + ``` + +=== "C++" + + ```cpp title="preorder_find_paths.cpp" + [class]{}-[func]{preOrder} + ``` + +=== "Python" + + ```python title="preorder_find_paths.py" + def pre_order(root: TreeNode) -> None: + """前序遍历""" + if root is None: + return + # 尝试 + path.append(root) + if root.val == 7: + # 记录解 + res.append(list(path)) + pre_order(root.left) + pre_order(root.right) + # 回退 + path.pop() + return + ``` + +=== "Go" + + ```go title="preorder_find_paths.go" + [class]{}-[func]{preOrder} + ``` + +=== "JavaScript" + + ```javascript title="preorder_find_paths.js" + [class]{}-[func]{preOrder} + ``` + +=== "TypeScript" + + ```typescript title="preorder_find_paths.ts" + [class]{}-[func]{preOrder} + ``` + +=== "C" + + ```c title="preorder_find_paths.c" + [class]{}-[func]{preOrder} + ``` + +=== "C#" + + ```csharp title="preorder_find_paths.cs" + [class]{preorder_find_paths}-[func]{preOrder} + ``` + +=== "Swift" + + ```swift title="preorder_find_paths.swift" + [class]{}-[func]{preOrder} + ``` + +=== "Zig" + + ```zig title="preorder_find_paths.zig" + [class]{}-[func]{preOrder} + ``` + +在每次“尝试”中,我们通过将当前节点添加进 `path` 来记录路径;而在“回退”前,我们需要将该节点从 `path` 中弹出,**以恢复本次尝试之前的状态**。换句话说,**我们可以将尝试和回退理解为“前进”与“撤销”**,两个操作是互为相反的。 + +=== "<1>" +  + +=== "<2>" +  + +=== "<3>" +  + +=== "<4>" +  + +=== "<5>" +  + +=== "<6>" +  + +=== "<7>" +  + +=== "<8>" +  + +=== "<9>" +  + +=== "<10>" +  + +=== "<11>" +  + +## 12.1.2. 剪枝 + +复杂的回溯问题通常包含一个或多个约束条件,**约束条件通常可用于“剪枝”**。 + +!!! question "例题三:在二叉树中搜索所有值为 $7$ 的节点,返回根节点到这些节点的路径,**路径中不能包含值为 $3$ 的节点**" + +**解题思路**:在例题二的基础上添加剪枝操作,当遇到值为 $3$ 的节点时,则终止继续搜索。 + +=== "Java" + + ```java title="preorder_find_constrained_paths.java" + /* 前序遍历 */ + void preOrder(TreeNode root) { + // 剪枝 + if (root == null || root.val == 3) { + return; + } + // 尝试 + path.add(root); + if (root.val == 7) { + // 记录解 + res.add(new ArrayList<>(path)); + } + preOrder(root.left); + preOrder(root.right); + // 回退 + path.remove(path.size() - 1); + } + ``` + +=== "C++" + + ```cpp title="preorder_find_constrained_paths.cpp" + [class]{}-[func]{preOrder} + ``` + +=== "Python" + + ```python title="preorder_find_constrained_paths.py" + def pre_order(root: TreeNode) -> None: + """前序遍历""" + # 剪枝 + if root is None or root.val == 3: + return + # 尝试 + path.append(root) + if root.val == 7: + # 记录解 + res.append(list(path)) + pre_order(root.left) + pre_order(root.right) + # 回退 + path.pop() + return + ``` + +=== "Go" + + ```go title="preorder_find_constrained_paths.go" + [class]{}-[func]{preOrder} + ``` + +=== "JavaScript" + + ```javascript title="preorder_find_constrained_paths.js" + [class]{}-[func]{preOrder} + ``` + +=== "TypeScript" + + ```typescript title="preorder_find_constrained_paths.ts" + [class]{}-[func]{preOrder} + ``` + +=== "C" + + ```c title="preorder_find_constrained_paths.c" + [class]{}-[func]{preOrder} + ``` + +=== "C#" + + ```csharp title="preorder_find_constrained_paths.cs" + [class]{preorder_find_constrained_paths}-[func]{preOrder} + ``` + +=== "Swift" + + ```swift title="preorder_find_constrained_paths.swift" + [class]{}-[func]{preOrder} + ``` + +=== "Zig" + + ```zig title="preorder_find_constrained_paths.zig" + [class]{}-[func]{preOrder} + ``` + +剪枝是一个非常形象的名词。在搜索过程中,**我们利用约束条件“剪掉”了不满足约束条件的搜索分支**,避免许多无意义的尝试,从而提升搜索效率。 + + + +Fig. 根据约束条件剪枝
+ +## 12.1.3. 常用术语 + +为了更清晰地分析算法问题,我们总结一下回溯算法中常用术语的含义,并对照例题三给出对应示例。 + +| 名词 | 定义 | 例题三 | +| ------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| 解 Solution | 解是满足问题特定条件的答案。回溯算法的目标是找到一个或多个满足条件的解 | 根节点到节点 $7$ 的所有路径,且路径中不包含值为 $3$ 的节点 | +| 状态 State | 状态表示问题在某一时刻的情况,包括已经做出的选择 | 当前已访问的节点路径,即 `path` 节点列表 | +| 约束条件 Constraint | 约束条件是问题中限制解的可行性的条件,通常用于剪枝 | 要求路径中不能包含值为 $3$ 的节点 | +| 尝试 Attempt | 尝试是在搜索过程中,根据当前状态和可用选择来探索解空间的过程。尝试包括做出选择,更新状态,检查是否为解 | 递归访问左(右)子节点,将节点添加进 `path` ,判断节点的值是否为 $7$ | +| 回退 Backtracking | 回退指在搜索中遇到到不满足约束条件或无法继续搜索的状态时,撤销前面做出的选择,回到上一个状态 | 当越过叶结点、结束结点访问、遇到值为 $3$ 的节点时终止搜索,函数返回 | +| 剪枝 Pruning | 剪枝是根据问题特性和约束条件避免无意义的搜索路径的方法,可提高搜索效率 | 当遇到值为 $3$ 的节点时,则终止继续搜索 | + +!!! tip + + 解、状态、约束条件等术语是通用的,适用于回溯算法、动态规划、贪心算法等。 + +## 12.1.4. 框架代码 + +回溯算法可用于解决许多搜索问题、约束满足问题和组合优化问题。为提升代码通用性,我们希望将回溯算法的“尝试、回退、剪枝”的主体框架提炼出来。 + +设 `state` 为问题的当前状态,`choices` 表示当前状态下可以做出的选择,则可得到以下回溯算法的框架代码。 + +```python +def backtrack(state, choices, res): + """回溯算法框架""" + # 判断是否为解 + if is_solution(state): + # 记录解 + record_solution(state, res) + return + # 遍历所有选择 + for choice in choices: + # 剪枝:判断选择是否合法 + if is_valid(state, choice): + # 尝试:做出选择,更新状态 + make_choice(state, choice) + backtrack(state, choices, res) + # 回退:撤销选择,恢复到之前的状态 + undo_choice(state, choice) +``` + +下面,我们尝试基于此框架来解决例题三。在例题三中,状态 `state` 是节点遍历路径,选择 `choices` 是当前节点的左子节点和右子节点,结果 `res` 是路径列表,实现代码如下所示。 + +=== "Java" + + ```java title="backtrack_find_constrained_paths.java" + /* 判断当前状态是否为解 */ + boolean isSolution(ListFig. 排序中不同的元素类型和判断规则
+Fig. 数据类型和判断规则示例
## 11.1.1. 评价维度