mirror of
https://github.com/krahets/hello-algo.git
synced 2025-07-06 14:27:26 +08:00
feat: Revised the book (#978)
* Sync recent changes to the revised Word. * Revised the preface chapter * Revised the introduction chapter * Revised the computation complexity chapter * Revised the chapter data structure * Revised the chapter array and linked list * Revised the chapter stack and queue * Revised the chapter hashing * Revised the chapter tree * Revised the chapter heap * Revised the chapter graph * Revised the chapter searching * Reivised the sorting chapter * Revised the divide and conquer chapter * Revised the chapter backtacking * Revised the DP chapter * Revised the greedy chapter * Revised the appendix chapter * Revised the preface chapter doubly * Revised the figures
This commit is contained in:
@ -2,13 +2,13 @@
|
||||
|
||||
「回溯算法 backtracking algorithm」是一种通过穷举来解决问题的方法,它的核心思想是从一个初始状态出发,暴力搜索所有可能的解决方案,当遇到正确的解则将其记录,直到找到解或者尝试了所有可能的选择都无法找到解为止。
|
||||
|
||||
回溯算法通常采用“深度优先搜索”来遍历解空间。在二叉树章节中,我们提到前序、中序和后序遍历都属于深度优先搜索。接下来,我们利用前序遍历构造一个回溯问题,逐步了解回溯算法的工作原理。
|
||||
回溯算法通常采用“深度优先搜索”来遍历解空间。在“二叉树”章节中,我们提到前序、中序和后序遍历都属于深度优先搜索。接下来,我们利用前序遍历构造一个回溯问题,逐步了解回溯算法的工作原理。
|
||||
|
||||
!!! question "例题一"
|
||||
|
||||
给定一个二叉树,搜索并记录所有值为 $7$ 的节点,请返回节点列表。
|
||||
给定一棵二叉树,搜索并记录所有值为 $7$ 的节点,请返回节点列表。
|
||||
|
||||
对于此题,我们前序遍历这颗树,并判断当前节点的值是否为 $7$ ,若是则将该节点的值加入到结果列表 `res` 之中。相关过程实现如下图和以下代码所示。
|
||||
对于此题,我们前序遍历这棵树,并判断当前节点的值是否为 $7$ ,若是,则将该节点的值加入结果列表 `res` 之中。相关过程实现如下图和以下代码所示:
|
||||
|
||||
```src
|
||||
[file]{preorder_traversal_i_compact}-[class]{}-[func]{pre_order}
|
||||
@ -28,7 +28,7 @@
|
||||
|
||||
在二叉树中搜索所有值为 $7$ 的节点,**请返回根节点到这些节点的路径**。
|
||||
|
||||
在例题一代码的基础上,我们需要借助一个列表 `path` 记录访问过的节点路径。当访问到值为 $7$ 的节点时,则复制 `path` 并添加进结果列表 `res` 。遍历完成后,`res` 中保存的就是所有的解。
|
||||
在例题一代码的基础上,我们需要借助一个列表 `path` 记录访问过的节点路径。当访问到值为 $7$ 的节点时,则复制 `path` 并添加进结果列表 `res` 。遍历完成后,`res` 中保存的就是所有的解。代码如下所示:
|
||||
|
||||
```src
|
||||
[file]{preorder_traversal_ii_compact}-[class]{}-[func]{pre_order}
|
||||
@ -36,7 +36,7 @@
|
||||
|
||||
在每次“尝试”中,我们通过将当前节点添加进 `path` 来记录路径;而在“回退”前,我们需要将该节点从 `path` 中弹出,**以恢复本次尝试之前的状态**。
|
||||
|
||||
观察下图所示的过程,**我们可以将尝试和回退理解为“前进”与“撤销”**,两个操作是互为逆向的。
|
||||
观察下图所示的过程,**我们可以将尝试和回退理解为“前进”与“撤销”**,两个操作互为逆向。
|
||||
|
||||
=== "<1>"
|
||||

|
||||
@ -79,13 +79,13 @@
|
||||
|
||||
在二叉树中搜索所有值为 $7$ 的节点,请返回根节点到这些节点的路径,**并要求路径中不包含值为 $3$ 的节点**。
|
||||
|
||||
为了满足以上约束条件,**我们需要添加剪枝操作**:在搜索过程中,若遇到值为 $3$ 的节点,则提前返回,停止继续搜索。
|
||||
为了满足以上约束条件,**我们需要添加剪枝操作**:在搜索过程中,若遇到值为 $3$ 的节点,则提前返回,不再继续搜索。代码如下所示:
|
||||
|
||||
```src
|
||||
[file]{preorder_traversal_iii_compact}-[class]{}-[func]{pre_order}
|
||||
```
|
||||
|
||||
剪枝是一个非常形象的名词。如下图所示,在搜索过程中,**我们“剪掉”了不满足约束条件的搜索分支**,避免许多无意义的尝试,从而提高了搜索效率。
|
||||
“剪枝”是一个非常形象的名词。如下图所示,在搜索过程中,**我们“剪掉”了不满足约束条件的搜索分支**,避免许多无意义的尝试,从而提高了搜索效率。
|
||||
|
||||

|
||||
|
||||
@ -93,7 +93,7 @@
|
||||
|
||||
接下来,我们尝试将回溯的“尝试、回退、剪枝”的主体框架提炼出来,提升代码的通用性。
|
||||
|
||||
在以下框架代码中,`state` 表示问题的当前状态,`choices` 表示当前状态下可以做出的选择。
|
||||
在以下框架代码中,`state` 表示问题的当前状态,`choices` 表示当前状态下可以做出的选择:
|
||||
|
||||
=== "Python"
|
||||
|
||||
@ -104,7 +104,7 @@
|
||||
if is_solution(state):
|
||||
# 记录解
|
||||
record_solution(state, res)
|
||||
# 停止继续搜索
|
||||
# 不再继续搜索
|
||||
return
|
||||
# 遍历所有选择
|
||||
for choice in choices:
|
||||
@ -126,7 +126,7 @@
|
||||
if (isSolution(state)) {
|
||||
// 记录解
|
||||
recordSolution(state, res);
|
||||
// 停止继续搜索
|
||||
// 不再继续搜索
|
||||
return;
|
||||
}
|
||||
// 遍历所有选择
|
||||
@ -152,7 +152,7 @@
|
||||
if (isSolution(state)) {
|
||||
// 记录解
|
||||
recordSolution(state, res);
|
||||
// 停止继续搜索
|
||||
// 不再继续搜索
|
||||
return;
|
||||
}
|
||||
// 遍历所有选择
|
||||
@ -178,7 +178,7 @@
|
||||
if (IsSolution(state)) {
|
||||
// 记录解
|
||||
RecordSolution(state, res);
|
||||
// 停止继续搜索
|
||||
// 不再继续搜索
|
||||
return;
|
||||
}
|
||||
// 遍历所有选择
|
||||
@ -204,7 +204,7 @@
|
||||
if isSolution(state) {
|
||||
// 记录解
|
||||
recordSolution(state, res)
|
||||
// 停止继续搜索
|
||||
// 不再继续搜索
|
||||
return
|
||||
}
|
||||
// 遍历所有选择
|
||||
@ -230,7 +230,7 @@
|
||||
if isSolution(state: state) {
|
||||
// 记录解
|
||||
recordSolution(state: state, res: &res)
|
||||
// 停止继续搜索
|
||||
// 不再继续搜索
|
||||
return
|
||||
}
|
||||
// 遍历所有选择
|
||||
@ -256,7 +256,7 @@
|
||||
if (isSolution(state)) {
|
||||
// 记录解
|
||||
recordSolution(state, res);
|
||||
// 停止继续搜索
|
||||
// 不再继续搜索
|
||||
return;
|
||||
}
|
||||
// 遍历所有选择
|
||||
@ -282,7 +282,7 @@
|
||||
if (isSolution(state)) {
|
||||
// 记录解
|
||||
recordSolution(state, res);
|
||||
// 停止继续搜索
|
||||
// 不再继续搜索
|
||||
return;
|
||||
}
|
||||
// 遍历所有选择
|
||||
@ -308,7 +308,7 @@
|
||||
if (isSolution(state)) {
|
||||
// 记录解
|
||||
recordSolution(state, res);
|
||||
// 停止继续搜索
|
||||
// 不再继续搜索
|
||||
return;
|
||||
}
|
||||
// 遍历所有选择
|
||||
@ -334,7 +334,7 @@
|
||||
if is_solution(state) {
|
||||
// 记录解
|
||||
record_solution(state, res);
|
||||
// 停止继续搜索
|
||||
// 不再继续搜索
|
||||
return;
|
||||
}
|
||||
// 遍历所有选择
|
||||
@ -360,7 +360,7 @@
|
||||
if (isSolution(state)) {
|
||||
// 记录解
|
||||
recordSolution(state, res, numRes);
|
||||
// 停止继续搜索
|
||||
// 不再继续搜索
|
||||
return;
|
||||
}
|
||||
// 遍历所有选择
|
||||
@ -383,7 +383,7 @@
|
||||
|
||||
```
|
||||
|
||||
接下来,我们基于框架代码来解决例题三。状态 `state` 为节点遍历路径,选择 `choices` 为当前节点的左子节点和右子节点,结果 `res` 是路径列表。
|
||||
接下来,我们基于框架代码来解决例题三。状态 `state` 为节点遍历路径,选择 `choices` 为当前节点的左子节点和右子节点,结果 `res` 是路径列表:
|
||||
|
||||
```src
|
||||
[file]{preorder_traversal_iii_template}-[class]{}-[func]{backtrack}
|
||||
@ -393,37 +393,37 @@
|
||||
|
||||

|
||||
|
||||
相比基于前序遍历的代码实现,基于回溯算法框架的代码实现虽然显得啰嗦,但通用性更好。实际上,**许多回溯问题都可以在该框架下解决**。我们只需根据具体问题来定义 `state` 和 `choices` ,并实现框架中的各个方法即可。
|
||||
相比基于前序遍历的代码实现,基于回溯算法框架的代码实现虽然显得啰唆,但通用性更好。实际上,**许多回溯问题可以在该框架下解决**。我们只需根据具体问题来定义 `state` 和 `choices` ,并实现框架中的各个方法即可。
|
||||
|
||||
## 常用术语
|
||||
|
||||
为了更清晰地分析算法问题,我们总结一下回溯算法中常用术语的含义,并对照例题三给出对应示例。
|
||||
为了更清晰地分析算法问题,我们总结一下回溯算法中常用术语的含义,并对照例题三给出对应示例,如下表所示。
|
||||
|
||||
<p align="center"> 表 <id> 常见的回溯算法术语 </p>
|
||||
|
||||
| 名词 | 定义 | 例题三 |
|
||||
| ------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| 解 Solution | 解是满足问题特定条件的答案,可能有一个或多个 | 根节点到节点 $7$ 的满足约束条件的所有路径 |
|
||||
| 约束条件 Constraint | 约束条件是问题中限制解的可行性的条件,通常用于剪枝 | 路径中不包含节点 $3$ |
|
||||
| 状态 State | 状态表示问题在某一时刻的情况,包括已经做出的选择 | 当前已访问的节点路径,即 `path` 节点列表 |
|
||||
| 尝试 Attempt | 尝试是根据可用选择来探索解空间的过程,包括做出选择,更新状态,检查是否为解 | 递归访问左(右)子节点,将节点添加进 `path` ,判断节点的值是否为 $7$ |
|
||||
| 回退 Backtracking | 回退指遇到不满足约束条件的状态时,撤销前面做出的选择,回到上一个状态 | 当越过叶节点、结束节点访问、遇到值为 $3$ 的节点时终止搜索,函数返回 |
|
||||
| 剪枝 Pruning | 剪枝是根据问题特性和约束条件避免无意义的搜索路径的方法,可提高搜索效率 | 当遇到值为 $3$ 的节点时,则终止继续搜索 |
|
||||
| 名词 | 定义 | 例题三 |
|
||||
| ---------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| 解(solution) | 解是满足问题特定条件的答案,可能有一个或多个 | 根节点到节点 $7$ 的满足约束条件的所有路径 |
|
||||
| 约束条件(constraint) | 约束条件是问题中限制解的可行性的条件,通常用于剪枝 | 路径中不包含节点 $3$ |
|
||||
| 状态(state) | 状态表示问题在某一时刻的情况,包括已经做出的选择 | 当前已访问的节点路径,即 `path` 节点列表 |
|
||||
| 尝试(attempt) | 尝试是根据可用选择来探索解空间的过程,包括做出选择,更新状态,检查是否为解 | 递归访问左(右)子节点,将节点添加进 `path` ,判断节点的值是否为 $7$ |
|
||||
| 回退(backtracking) | 回退指遇到不满足约束条件的状态时,撤销前面做出的选择,回到上一个状态 | 当越过叶节点、结束节点访问、遇到值为 $3$ 的节点时终止搜索,函数返回 |
|
||||
| 剪枝(pruning) | 剪枝是根据问题特性和约束条件避免无意义的搜索路径的方法,可提高搜索效率 | 当遇到值为 $3$ 的节点时,则不再继续搜索 |
|
||||
|
||||
!!! tip
|
||||
|
||||
问题、解、状态等概念是通用的,在分治、回溯、动态规划、贪心等算法中都有涉及。
|
||||
|
||||
## 优势与局限性
|
||||
## 优点与局限性
|
||||
|
||||
回溯算法本质上是一种深度优先搜索算法,它尝试所有可能的解决方案直到找到满足条件的解。这种方法的优势在于它能够找到所有可能的解决方案,而且在合理的剪枝操作下,具有很高的效率。
|
||||
回溯算法本质上是一种深度优先搜索算法,它尝试所有可能的解决方案直到找到满足条件的解。这种方法的优点在于能够找到所有可能的解决方案,而且在合理的剪枝操作下,具有很高的效率。
|
||||
|
||||
然而,在处理大规模或者复杂问题时,**回溯算法的运行效率可能难以接受**。
|
||||
|
||||
- **时间**:回溯算法通常需要遍历状态空间的所有可能,时间复杂度可以达到指数阶或阶乘阶。
|
||||
- **空间**:在递归调用中需要保存当前的状态(例如路径、用于剪枝的辅助变量等),当深度很大时,空间需求可能会变得很大。
|
||||
|
||||
即便如此,**回溯算法仍然是某些搜索问题和约束满足问题的最佳解决方案**。对于这些问题,由于无法预测哪些选择可生成有效的解,因此我们必须对所有可能的选择进行遍历。在这种情况下,**关键是如何进行效率优化**,常见的效率优化方法有两种。
|
||||
即便如此,**回溯算法仍然是某些搜索问题和约束满足问题的最佳解决方案**。对于这些问题,由于无法预测哪些选择可生成有效的解,因此我们必须对所有可能的选择进行遍历。在这种情况下,**关键是如何优化效率**,常见的效率优化方法有两种。
|
||||
|
||||
- **剪枝**:避免搜索那些肯定不会产生解的路径,从而节省时间和空间。
|
||||
- **启发式搜索**:在搜索过程中引入一些策略或者估计值,从而优先搜索最有可能产生有效解的路径。
|
||||
@ -436,7 +436,7 @@
|
||||
|
||||
- 全排列问题:给定一个集合,求出其所有可能的排列组合。
|
||||
- 子集和问题:给定一个集合和一个目标和,找到集合中所有和为目标和的子集。
|
||||
- 汉诺塔问题:给定三个柱子和一系列大小不同的圆盘,要求将所有圆盘从一个柱子移动到另一个柱子,每次只能移动一个圆盘,且不能将大圆盘放在小圆盘上。
|
||||
- 汉诺塔问题:给定三根柱子和一系列大小不同的圆盘,要求将所有圆盘从一根柱子移动到另一根柱子,每次只能移动一个圆盘,且不能将大圆盘放在小圆盘上。
|
||||
|
||||
**约束满足问题**:这类问题的目标是找到满足所有约束条件的解。
|
||||
|
||||
@ -450,8 +450,8 @@
|
||||
- 旅行商问题:在一个图中,从一个点出发,访问所有其他点恰好一次后返回起点,求最短路径。
|
||||
- 最大团问题:给定一个无向图,找到最大的完全子图,即子图中的任意两个顶点之间都有边相连。
|
||||
|
||||
请注意,对于许多组合优化问题,回溯都不是最优解决方案。
|
||||
请注意,对于许多组合优化问题,回溯不是最优解决方案。
|
||||
|
||||
- 0-1 背包问题通常使用动态规划解决,以达到更高的时间效率。
|
||||
- 旅行商是一个著名的 NP-Hard 问题,常用解法有遗传算法和蚁群算法等。
|
||||
- 最大团问题是图论中的一个经典问题,可用贪心等启发式算法来解决。
|
||||
- 最大团问题是图论中的一个经典问题,可用贪心算法等启发式算法来解决。
|
||||
|
Reference in New Issue
Block a user