|
|
@ -201,12 +201,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
!!! question "例题三"
|
|
|
|
!!! question "例题三"
|
|
|
|
|
|
|
|
|
|
|
|
在二叉树中搜索所有值为 $7$ 的节点,请返回根节点到这些节点的路径,**要求路径中只存在一个值为 $7$ 的节点,并且不允许有值为 $3$ 的节点**。
|
|
|
|
在二叉树中搜索所有值为 $7$ 的节点,请返回根节点到这些节点的路径,**并要求路径中不包含值为 $3$ 的节点**。
|
|
|
|
|
|
|
|
|
|
|
|
在例题二的基础上添加剪枝操作,包括:
|
|
|
|
为了满足以上约束条件,**我们需要添加剪枝操作**:在搜索过程中,若遇到值为 $3$ 的节点,则提前返回,停止继续搜索。
|
|
|
|
|
|
|
|
|
|
|
|
- 当遇到值为 $7$ 的节点时,记录解并返回,停止搜索。
|
|
|
|
|
|
|
|
- 当遇到值为 $3$ 的节点时,则直接返回,停止搜索。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
=== "Java"
|
|
|
|
=== "Java"
|
|
|
|
|
|
|
|
|
|
|
@ -274,27 +271,10 @@
|
|
|
|
[class]{}-[func]{preOrder}
|
|
|
|
[class]{}-[func]{preOrder}
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
剪枝是一个非常形象的名词。在搜索过程中,**我们利用约束条件“剪掉”了不满足约束条件的搜索分支**,避免许多无意义的尝试,从而提升搜索效率。
|
|
|
|
剪枝是一个非常形象的名词。在搜索过程中,**我们“剪掉”了不满足约束条件的搜索分支**,避免许多无意义的尝试,从而实现搜索效率的提高。
|
|
|
|
|
|
|
|
|
|
|
|
![根据约束条件剪枝](backtracking_algorithm.assets/preorder_find_constrained_paths.png)
|
|
|
|
![根据约束条件剪枝](backtracking_algorithm.assets/preorder_find_constrained_paths.png)
|
|
|
|
|
|
|
|
|
|
|
|
## 常用术语
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
为了更清晰地分析算法问题,我们总结一下回溯算法中常用术语的含义,并对照例题三给出对应示例。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 名词 | 定义 | 例题三 |
|
|
|
|
|
|
|
|
| ------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
|
|
|
|
|
|
| 解 Solution | 解是满足问题特定条件的答案,可能有一个或多个 | 根节点到节点 $7$ 的满足约束条件的所有路径 |
|
|
|
|
|
|
|
|
| 约束条件 Constraint | 约束条件是问题中限制解的可行性的条件,通常用于剪枝 | 路径中不包含节点 $3$ ,只包含一个节点 $7$ |
|
|
|
|
|
|
|
|
| 状态 State | 状态表示问题在某一时刻的情况,包括已经做出的选择 | 当前已访问的节点路径,即 `path` 节点列表 |
|
|
|
|
|
|
|
|
| 尝试 Attempt | 尝试是根据可用选择来探索解空间的过程,包括做出选择,更新状态,检查是否为解 | 递归访问左(右)子节点,将节点添加进 `path` ,判断节点的值是否为 $7$ |
|
|
|
|
|
|
|
|
| 回退 Backtracking | 回退指遇到不满足约束条件的状态时,撤销前面做出的选择,回到上一个状态 | 当越过叶结点、结束结点访问、遇到值为 $3$ 的节点时终止搜索,函数返回 |
|
|
|
|
|
|
|
|
| 剪枝 Pruning | 剪枝是根据问题特性和约束条件避免无意义的搜索路径的方法,可提高搜索效率 | 当遇到值为 $3$ 的节点时,则终止继续搜索 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
!!! tip
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
问题、解、状态等概念是通用的,在分治、回溯、动态规划、贪心等算法中都有涉及。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 框架代码
|
|
|
|
## 框架代码
|
|
|
|
|
|
|
|
|
|
|
|
接下来,我们尝试将回溯的“尝试、回退、剪枝”的主体框架提炼出来,提升代码的通用性。
|
|
|
|
接下来,我们尝试将回溯的“尝试、回退、剪枝”的主体框架提炼出来,提升代码的通用性。
|
|
|
@ -310,6 +290,7 @@
|
|
|
|
if (isSolution(state)) {
|
|
|
|
if (isSolution(state)) {
|
|
|
|
// 记录解
|
|
|
|
// 记录解
|
|
|
|
recordSolution(state, res);
|
|
|
|
recordSolution(state, res);
|
|
|
|
|
|
|
|
// 停止继续搜索
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// 遍历所有选择
|
|
|
|
// 遍历所有选择
|
|
|
@ -335,6 +316,7 @@
|
|
|
|
if (isSolution(state)) {
|
|
|
|
if (isSolution(state)) {
|
|
|
|
// 记录解
|
|
|
|
// 记录解
|
|
|
|
recordSolution(state, res);
|
|
|
|
recordSolution(state, res);
|
|
|
|
|
|
|
|
// 停止继续搜索
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// 遍历所有选择
|
|
|
|
// 遍历所有选择
|
|
|
@ -354,12 +336,13 @@
|
|
|
|
=== "Python"
|
|
|
|
=== "Python"
|
|
|
|
|
|
|
|
|
|
|
|
```python title=""
|
|
|
|
```python title=""
|
|
|
|
def backtrack(state: State, choices: list[choice], res: list[state]) -> None:
|
|
|
|
def backtrack(state: State, choices: list[choice], res: list[state]):
|
|
|
|
"""回溯算法框架"""
|
|
|
|
"""回溯算法框架"""
|
|
|
|
# 判断是否为解
|
|
|
|
# 判断是否为解
|
|
|
|
if is_solution(state):
|
|
|
|
if is_solution(state):
|
|
|
|
# 记录解
|
|
|
|
# 记录解
|
|
|
|
record_solution(state, res)
|
|
|
|
record_solution(state, res)
|
|
|
|
|
|
|
|
# 停止继续搜索
|
|
|
|
return
|
|
|
|
return
|
|
|
|
# 遍历所有选择
|
|
|
|
# 遍历所有选择
|
|
|
|
for choice in choices:
|
|
|
|
for choice in choices:
|
|
|
@ -381,6 +364,7 @@
|
|
|
|
if isSolution(state) {
|
|
|
|
if isSolution(state) {
|
|
|
|
// 记录解
|
|
|
|
// 记录解
|
|
|
|
recordSolution(state, res)
|
|
|
|
recordSolution(state, res)
|
|
|
|
|
|
|
|
// 停止继续搜索
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// 遍历所有选择
|
|
|
|
// 遍历所有选择
|
|
|
@ -406,6 +390,7 @@
|
|
|
|
if (isSolution(state)) {
|
|
|
|
if (isSolution(state)) {
|
|
|
|
// 记录解
|
|
|
|
// 记录解
|
|
|
|
recordSolution(state, res);
|
|
|
|
recordSolution(state, res);
|
|
|
|
|
|
|
|
// 停止继续搜索
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// 遍历所有选择
|
|
|
|
// 遍历所有选择
|
|
|
@ -431,6 +416,7 @@
|
|
|
|
if (isSolution(state)) {
|
|
|
|
if (isSolution(state)) {
|
|
|
|
// 记录解
|
|
|
|
// 记录解
|
|
|
|
recordSolution(state, res);
|
|
|
|
recordSolution(state, res);
|
|
|
|
|
|
|
|
// 停止继续搜索
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// 遍历所有选择
|
|
|
|
// 遍历所有选择
|
|
|
@ -456,6 +442,7 @@
|
|
|
|
if (isSolution(state)) {
|
|
|
|
if (isSolution(state)) {
|
|
|
|
// 记录解
|
|
|
|
// 记录解
|
|
|
|
recordSolution(state, res, numRes);
|
|
|
|
recordSolution(state, res, numRes);
|
|
|
|
|
|
|
|
// 停止继续搜索
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// 遍历所有选择
|
|
|
|
// 遍历所有选择
|
|
|
@ -481,6 +468,7 @@
|
|
|
|
if (isSolution(state)) {
|
|
|
|
if (isSolution(state)) {
|
|
|
|
// 记录解
|
|
|
|
// 记录解
|
|
|
|
recordSolution(state, res);
|
|
|
|
recordSolution(state, res);
|
|
|
|
|
|
|
|
// 停止继续搜索
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// 遍历所有选择
|
|
|
|
// 遍历所有选择
|
|
|
@ -506,6 +494,7 @@
|
|
|
|
if isSolution(state: state) {
|
|
|
|
if isSolution(state: state) {
|
|
|
|
// 记录解
|
|
|
|
// 记录解
|
|
|
|
recordSolution(state: state, res: &res)
|
|
|
|
recordSolution(state: state, res: &res)
|
|
|
|
|
|
|
|
// 停止继续搜索
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// 遍历所有选择
|
|
|
|
// 遍历所有选择
|
|
|
@ -537,6 +526,7 @@
|
|
|
|
if (isSolution(state)) {
|
|
|
|
if (isSolution(state)) {
|
|
|
|
// 记录解
|
|
|
|
// 记录解
|
|
|
|
recordSolution(state, res);
|
|
|
|
recordSolution(state, res);
|
|
|
|
|
|
|
|
// 停止继续搜索
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// 遍历所有选择
|
|
|
|
// 遍历所有选择
|
|
|
@ -553,7 +543,7 @@
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
下面,我们基于框架代码来解决例题三:状态 `state` 为节点遍历路径,选择 `choices` 为当前节点的左子节点和右子节点,结果 `res` 是路径列表。
|
|
|
|
接下来,我们基于框架代码来解决例题三。状态 `state` 为节点遍历路径,选择 `choices` 为当前节点的左子节点和右子节点,结果 `res` 是路径列表。
|
|
|
|
|
|
|
|
|
|
|
|
=== "Java"
|
|
|
|
=== "Java"
|
|
|
|
|
|
|
|
|
|
|
@ -731,7 +721,28 @@
|
|
|
|
[class]{}-[func]{backtrack}
|
|
|
|
[class]{}-[func]{backtrack}
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
相比基于前序遍历的代码实现,基于回溯算法框架的代码实现虽然显得啰嗦,但通用性更好。实际上,**许多回溯问题都可以在该框架下解决**。我们只需根据具体问题来定义 `state` 和 `choices` ,并实现框架中的各个方法。
|
|
|
|
根据题意,当找到值为 7 的节点后应该继续搜索,**因此我们需要将记录解之后的 `return` 语句删除**。下图对比了保留或删除 `return` 语句的搜索过程。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
![保留与删除 return 的搜索过程对比](backtracking_algorithm.assets/backtrack_remove_return_or_not.png)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
相比基于前序遍历的代码实现,基于回溯算法框架的代码实现虽然显得啰嗦,但通用性更好。实际上,**许多回溯问题都可以在该框架下解决**。我们只需根据具体问题来定义 `state` 和 `choices` ,并实现框架中的各个方法即可。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 常用术语
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
为了更清晰地分析算法问题,我们总结一下回溯算法中常用术语的含义,并对照例题三给出对应示例。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 名词 | 定义 | 例题三 |
|
|
|
|
|
|
|
|
| ------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
|
|
|
|
|
|
| 解 Solution | 解是满足问题特定条件的答案,可能有一个或多个 | 根节点到节点 $7$ 的满足约束条件的所有路径 |
|
|
|
|
|
|
|
|
| 约束条件 Constraint | 约束条件是问题中限制解的可行性的条件,通常用于剪枝 | 路径中不包含节点 $3$ ,只包含一个节点 $7$ |
|
|
|
|
|
|
|
|
| 状态 State | 状态表示问题在某一时刻的情况,包括已经做出的选择 | 当前已访问的节点路径,即 `path` 节点列表 |
|
|
|
|
|
|
|
|
| 尝试 Attempt | 尝试是根据可用选择来探索解空间的过程,包括做出选择,更新状态,检查是否为解 | 递归访问左(右)子节点,将节点添加进 `path` ,判断节点的值是否为 $7$ |
|
|
|
|
|
|
|
|
| 回退 Backtracking | 回退指遇到不满足约束条件的状态时,撤销前面做出的选择,回到上一个状态 | 当越过叶结点、结束结点访问、遇到值为 $3$ 的节点时终止搜索,函数返回 |
|
|
|
|
|
|
|
|
| 剪枝 Pruning | 剪枝是根据问题特性和约束条件避免无意义的搜索路径的方法,可提高搜索效率 | 当遇到值为 $3$ 的节点时,则终止继续搜索 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
!!! tip
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
问题、解、状态等概念是通用的,在分治、回溯、动态规划、贪心等算法中都有涉及。
|
|
|
|
|
|
|
|
|
|
|
|
## 优势与局限性
|
|
|
|
## 优势与局限性
|
|
|
|
|
|
|
|
|
|
|
|