2023-02-01 10:09:19 +00:00
|
|
|
|
# 总结
|
|
|
|
|
|
2023-02-03 10:07:50 +00:00
|
|
|
|
使用场景:如果解决一个问题需要多个步骤,而每个步骤都在前一步的基础上进行选择,那么就可以用回溯法。
|
2023-02-01 10:09:19 +00:00
|
|
|
|
|
2023-02-03 10:07:50 +00:00
|
|
|
|
回溯法本质是在一棵树上进行深度优先遍历,因此需要设计好这棵树是如何生成的。
|
2023-02-01 10:09:19 +00:00
|
|
|
|
|
|
|
|
|
算法设计:
|
|
|
|
|
|
|
|
|
|
关键就是要学会分析和画图,然后确定传入参数。只要传入参数确定了代码框架就确定了 (返回值一般是 `void`):
|
|
|
|
|
|
2023-02-02 09:34:34 +00:00
|
|
|
|
- 思考这棵树怎么画,每层遍历的逻辑是什么,每条边的操作逻辑是什么。
|
|
|
|
|
- 得设计一个数据结构 `NodeState` 来存放当前节点状态。该数据结构的可扩展性必须要强,需要满足以下条件:
|
|
|
|
|
- 能描述当前节点的状态
|
|
|
|
|
- 能作为最终结果存储
|
|
|
|
|
- 能根据当前节点更新状态和撤销之前的更改
|
|
|
|
|
- 得有一个 `&result` 来存放结果,这个 `&result` 通常是一个向量 `vector<NodeState> &`,里面存放了节点状态。
|
|
|
|
|
- 其它传入参数用来完成每层遍历操作和每条边的操作。
|
|
|
|
|
|
|
|
|
|
设计完了数据结构之后来看看具体代码怎么写。模板如下:
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
void backtrack(NodeState &node, vector<NodeState> &result, int para1, int para2, int para3) {
|
|
|
|
|
// 终止条件
|
|
|
|
|
// 回溯法中的每个节点并不是真的树状节点,没有 `nullptr` ,因此用空指针来判断是否到了叶子结点并不合理,需要其它的一些方法来确定是否到达叶子节点,比如高度。
|
|
|
|
|
if (/* end condition */) {
|
|
|
|
|
result.push_back(node);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-02 10:13:48 +00:00
|
|
|
|
// 剪枝
|
|
|
|
|
// 当现在的节点不可能出现我们想要的结果时,直接跳过。
|
|
|
|
|
if (/* out of scope */) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-02 09:34:34 +00:00
|
|
|
|
// 遍历该节点的所有子节点,即遍历下一层
|
|
|
|
|
for (...) {
|
2023-02-02 10:13:48 +00:00
|
|
|
|
// 剪枝也可以在 for 循环中完成
|
2023-02-02 09:34:34 +00:00
|
|
|
|
if (/* out of scope */) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
// 处理节点
|
|
|
|
|
// 现在 node 中的数据描述的是当前节点,
|
|
|
|
|
// handle(node) 一般是让 node 中的数据变成子节点的数据
|
|
|
|
|
handle(node);
|
|
|
|
|
// 递归
|
|
|
|
|
backtrack(node, result, para1, para2, para3);
|
|
|
|
|
// 撤销数据处理,让 node 中的数据再次变回描述当前节点的数据
|
|
|
|
|
revert(node);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
2023-02-01 10:09:19 +00:00
|
|
|
|
|
|
|
|
|
复杂度分析:
|
|
|
|
|
|
|
|
|
|
- 时间复杂度:最长路径长度 × 搜索树的节点数
|
|
|
|
|
- 空间复杂度:一个节点所需要的空间 × 搜索树的节点数
|
|
|
|
|
|