Finetune the chapter of hashing,

divide and conquer, backtracking, tree
This commit is contained in:
krahets
2023-07-24 03:04:55 +08:00
parent 9d56622c75
commit 17f995b432
16 changed files with 258 additions and 223 deletions

View File

@@ -2,11 +2,11 @@
「回溯算法 Backtracking Algorithm」是一种通过穷举来解决问题的方法它的核心思想是从一个初始状态出发暴力搜索所有可能的解决方案当遇到正确的解则将其记录直到找到解或者尝试了所有可能的选择都无法找到解为止。
回溯算法通常采用「深度优先搜索」来遍历解空间。在二叉树章节中,我们提到前序、中序和后序遍历都属于深度优先搜索。接下来我们用前序遍历构造一个回溯问题,逐步了解回溯算法的工作原理。
回溯算法通常采用「深度优先搜索」来遍历解空间。在二叉树章节中,我们提到前序、中序和后序遍历都属于深度优先搜索。接下来我们用前序遍历构造一个回溯问题,逐步了解回溯算法的工作原理。
!!! question "例题一"
给定一个二叉树,搜索并记录所有值为 $7$ 的节点,返回节点列表。
给定一个二叉树,搜索并记录所有值为 $7$ 的节点,返回节点列表。
对于此题,我们前序遍历这颗树,并判断当前节点的值是否为 $7$ ,若是则将该节点的值加入到结果列表 `res` 之中。
@@ -84,11 +84,11 @@
对于例题一,访问每个节点都代表一次“尝试”,而越过叶结点或返回父节点的 `return` 则表示“回退”。
值得说明的是,**回退并不等价于函数返回**。为解释这一点,我们对例题一稍作拓展。
值得说明的是,**回退并不仅仅包括函数返回**。为解释这一点,我们对例题一稍作拓展。
!!! question "例题二"
在二叉树中搜索所有值为 $7$ 的节点,**返回根节点到这些节点的路径**。
在二叉树中搜索所有值为 $7$ 的节点,**返回根节点到这些节点的路径**。
在例题一代码的基础上,我们需要借助一个列表 `path` 记录访问过的节点路径。当访问到值为 $7$ 的节点时,则复制 `path` 并添加进结果列表 `res` 。遍历完成后,`res` 中保存的就是所有的解。
@@ -158,7 +158,9 @@
[class]{}-[func]{preOrder}
```
在每次“尝试”中,我们通过将当前节点添加进 `path` 来记录路径;而在“回退”前,我们需要将该节点从 `path` 中弹出,**以恢复本次尝试之前的状态**。换句话说,**我们可以将尝试和回退理解为“前进”与“撤销”**,两个操作是互为相反的。
在每次“尝试”中,我们通过将当前节点添加进 `path` 来记录路径;而在“回退”前,我们需要将该节点从 `path` 中弹出,**以恢复本次尝试之前的状态**。
观察该过程,**我们可以将尝试和回退理解为“前进”与“撤销”**,两个操作是互为逆向的。
=== "<1>"
![尝试与回退](backtracking_algorithm.assets/preorder_find_paths_step1.png)
@@ -199,12 +201,12 @@
!!! question "例题三"
在二叉树中搜索所有值为 $7$ 的节点,返回根节点到这些节点的路径,**要求路径中有且只有一个值为 $7$ 的节点,并且不能包含值为 $3$ 的节点**。
在二叉树中搜索所有值为 $7$ 的节点,返回根节点到这些节点的路径,**要求路径中只存在一个值为 $7$ 的节点,并且不允许有值为 $3$ 的节点**。
在例题二的基础上添加剪枝操作,包括:
- 当遇到值为 $7$ 的节点时,记录解并返回,止搜索。
- 当遇到值为 $3$ 的节点时,则直接返回,停止继续搜索。
- 当遇到值为 $7$ 的节点时,记录解并返回,止搜索。
- 当遇到值为 $3$ 的节点时,则直接返回,停止搜索。
=== "Java"
@@ -280,24 +282,24 @@
为了更清晰地分析算法问题,我们总结一下回溯算法中常用术语的含义,并对照例题三给出对应示例。
| 名词 | 定义 | 例题三 |
| ------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
| 解 Solution | 解是满足问题特定条件的答案。回溯算法的目标是找到一个或多个满足条件的解 | 根节点到节点 $7$ 的所有路径,且路径中不包含值为 $3$ 的节点 |
| 状态 State | 状态表示问题在某一时刻的情况,包括已经做出的选择 | 当前已访问的节点路径,即 `path` 节点列表 |
| 约束条件 Constraint | 约束条件是问题中限制解的可行性的条件,通常用于剪枝 | 要求路径中不能包含值为 $3$ 的节点 |
| 尝试 Attempt | 尝试是在搜索过程中,根据当前状态和可用选择来探索解空间的过程。尝试包括做出选择,更新状态,检查是否为解 | 递归访问左(右)子节点,将节点添加进 `path` ,判断节点的值是否为 $7$ |
| 回退 Backtracking | 回退指在搜索中遇到不满足约束条件或无法继续搜索的状态时,撤销前面做出的选择,回到上一个状态 | 当越过叶结点、结束结点访问、遇到值为 $3$ 的节点时终止搜索,函数返回 |
| 剪枝 Pruning | 剪枝是根据问题特性和约束条件避免无意义的搜索路径的方法,可提高搜索效率 | 当遇到值为 $3$ 的节点时,则终止继续搜索 |
| 名词 | 定义 | 例题三 |
| ------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| 解 Solution | 解是满足问题特定条件的答案,可能有一个或多个 | 根节点到节点 $7$ 的满足约束条件的所有路径 |
| 约束条件 Constraint | 约束条件是问题中限制解的可行性的条件,通常用于剪枝 | 路径中不包含节点 $3$ ,只包含一个节点 $7$ |
| 状态 State | 状态表示问题在某一时刻的情况,包括已经做出的选择 | 当前已访问的节点路径,即 `path` 节点列表 |
| 尝试 Attempt | 尝试是根据可用选择来探索解空间的过程包括做出选择,更新状态,检查是否为解 | 递归访问左(右)子节点,将节点添加进 `path` ,判断节点的值是否为 $7$ |
| 回退 Backtracking | 回退指遇到不满足约束条件的状态时,撤销前面做出的选择,回到上一个状态 | 当越过叶结点、结束结点访问、遇到值为 $3$ 的节点时终止搜索,函数返回 |
| 剪枝 Pruning | 剪枝是根据问题特性和约束条件避免无意义的搜索路径的方法,可提高搜索效率 | 当遇到值为 $3$ 的节点时,则终止继续搜索 |
!!! tip
解、状态、约束条件等术语是通用的,适用于回溯算法、动态规划、贪心算法
问题、解、状态等概念是通用的,在分治、回溯、动态规划、贪心算法中都有涉及
## 框架代码
回溯算法可用于解决许多搜索问题、约束满足问题和组合优化问题。为提升代码通用性,我们希望将回溯算法的“尝试、回退、剪枝”的主体框架提炼出来。
接下来,我们尝试将回溯的“尝试、回退、剪枝”的主体框架提炼出来,提升代码的通用性
`state` 问题的当前状态,`choices` 表示当前状态下可以做出的选择,则可得到以下回溯算法的框架代码
在以下框架代码中,`state` 表示问题的当前状态,`choices` 表示当前状态下可以做出的选择。
=== "Java"
@@ -551,7 +553,7 @@
}
```
下面,我们尝试基于框架来解决例题三。在例题三中,状态 `state` 节点遍历路径,选择 `choices` 当前节点的左子节点和右子节点,结果 `res` 是路径列表,实现代码如下所示
下面,我们基于框架代码来解决例题三状态 `state` 节点遍历路径,选择 `choices` 当前节点的左子节点和右子节点,结果 `res` 是路径列表。
=== "Java"
@@ -729,7 +731,7 @@
[class]{}-[func]{backtrack}
```
较于基于前序遍历的实现代码,基于回溯算法框架的实现代码虽然显得啰嗦,但通用性更好。实际上,**所有回溯问题都可以在该框架下解决**。我们需根据具体问题来定义 `state` 和 `choices` ,并实现框架中的各个方法。
基于前序遍历的代码实现,基于回溯算法框架的代码实现虽然显得啰嗦,但通用性更好。实际上,**许多回溯问题都可以在该框架下解决**。我们需根据具体问题来定义 `state` 和 `choices` ,并实现框架中的各个方法。
## 优势与局限性
@@ -737,16 +739,18 @@
然而,在处理大规模或者复杂问题时,**回溯算法的运行效率可能难以接受**。
- 在最坏的情况下,回溯算法需要遍历空间的所有可能解,所需时间很长。例如,求解 $n$ 皇后问题的时间复杂度可以达到 $O(n!)$
-每一次递归调用时,都需要保存当前的状态(例如选择路径、用于剪枝的辅助变量等),对于深度很大的递归,空间需求可能会变得非常大。
- **时间**回溯算法通常需要遍历状态空间的所有可能,时间复杂度可以达到指数阶或阶乘阶
- **空间**在递归调用需要保存当前的状态(例如路径、用于剪枝的辅助变量等),深度很大,空间需求可能会变得大。
即便如此,**回溯算法仍然是某些搜索问题和约束满足问题的最佳解决方案**。对于这些问题,由于无法预测哪些选择可生成有效的解,因此我们必须对所有可能的选择进行遍历。在这种情况下,**关键是如何进行效率优化**
即便如此,**回溯算法仍然是某些搜索问题和约束满足问题的最佳解决方案**。对于这些问题,由于无法预测哪些选择可生成有效的解,因此我们必须对所有可能的选择进行遍历。在这种情况下,**关键是如何进行效率优化**,常见方法有
- 上文介绍过的剪枝是一种常用的优化方法。它可以避免搜索那些肯定不会产生有效解的路径,从而节省时间和空间。
- 另一个常用的优化方法是加入「启发式搜索 Heuristic Search」策略在搜索过程中引入一些策略或者估计值,从而优先搜索最有可能产生有效解的路径。
- **剪枝**避免搜索那些肯定不会产生解的路径,从而节省时间和空间。
- **启发式搜索**在搜索过程中引入一些策略或者估计值,从而优先搜索最有可能产生有效解的路径。
## 回溯典型例题
回溯算法可用于解决许多搜索问题、约束满足问题和组合优化问题。
**搜索问题**:这类问题的目标是找到满足特定条件的解决方案。
- 全排列问题:给定一个集合,求出其所有可能的排列组合。
@@ -765,4 +769,8 @@
- 旅行商问题:在一个图中,从一个点出发,访问所有其他点恰好一次后返回起点,求最短路径。
- 最大团问题:给定一个无向图,找到最大的完全子图,即子图中的任意两个顶点之间都有边相连。
请注意,回溯算法通常不是解决组合优化问题的最优方法。0-1 背包问题通常使用动态规划解决;旅行商是一个 NP-Hard 问题,常用解决方法有遗传算法和蚁群算法等;最大团问题是图轮中的一个经典 NP-Hard 问题,通常用贪心算法等启发式算法来解决。
请注意,对于许多组合优化问题,回溯都不是最优解决方案,例如:
- 0-1 背包问题通常使用动态规划解决,以达到更高的时间效率;
- 旅行商是一个著名的 NP-Hard 问题,常用解法有遗传算法和蚁群算法等;
- 最大团问题是图论中的一个经典问题,可用贪心等启发式算法来解决;