图 13-1 在前序遍历中搜索节点
## 13.1.1 尝试与回退 **之所以称之为回溯算法,是因为该算法在搜索解空间时会采用“尝试”与“回退”的策略**。当算法在搜索过程中遇到某个状态无法继续前进或无法得到满足条件的解时,它会撤销上一步的选择,退回到之前的状态,并尝试其他可能的选择。 对于例题一,访问每个节点都代表一次“尝试”,而越过叶节点或返回父节点的 `return` 则表示“回退”。 值得说明的是,**回退并不仅仅包括函数返回**。为解释这一点,我们对例题一稍作拓展。 !!! question "例题二" 在二叉树中搜索所有值为 $7$ 的节点,**请返回根节点到这些节点的路径**。 在例题一代码的基础上,我们需要借助一个列表 `path` 记录访问过的节点路径。当访问到值为 $7$ 的节点时,则复制 `path` 并添加进结果列表 `res` 。遍历完成后,`res` 中保存的就是所有的解。代码如下所示: === "Python" ```python title="preorder_traversal_ii_compact.py" def pre_order(root: TreeNode): """前序遍历:例题二""" 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() ``` === "C++" ```cpp title="preorder_traversal_ii_compact.cpp" /* 前序遍历:例题二 */ void preOrder(TreeNode *root) { if (root == nullptr) { return; } // 尝试 path.push_back(root); if (root->val == 7) { // 记录解 res.push_back(path); } preOrder(root->left); preOrder(root->right); // 回退 path.pop_back(); } ``` === "Java" ```java title="preorder_traversal_ii_compact.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#" ```csharp title="preorder_traversal_ii_compact.cs" /* 前序遍历:例题二 */ void PreOrder(TreeNode? root) { if (root == null) { return; } // 尝试 path.Add(root); if (root.val == 7) { // 记录解 res.Add(new List图 13-2 尝试与回退
## 13.1.2 剪枝 复杂的回溯问题通常包含一个或多个约束条件,**约束条件通常可用于“剪枝”**。 !!! question "例题三" 在二叉树中搜索所有值为 $7$ 的节点,请返回根节点到这些节点的路径,**并要求路径中不包含值为 $3$ 的节点**。 为了满足以上约束条件,**我们需要添加剪枝操作**:在搜索过程中,若遇到值为 $3$ 的节点,则提前返回,不再继续搜索。代码如下所示: === "Python" ```python title="preorder_traversal_iii_compact.py" def pre_order(root: TreeNode): """前序遍历:例题三""" # 剪枝 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() ``` === "C++" ```cpp title="preorder_traversal_iii_compact.cpp" /* 前序遍历:例题三 */ void preOrder(TreeNode *root) { // 剪枝 if (root == nullptr || root->val == 3) { return; } // 尝试 path.push_back(root); if (root->val == 7) { // 记录解 res.push_back(path); } preOrder(root->left); preOrder(root->right); // 回退 path.pop_back(); } ``` === "Java" ```java title="preorder_traversal_iii_compact.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#" ```csharp title="preorder_traversal_iii_compact.cs" /* 前序遍历:例题三 */ void PreOrder(TreeNode? root) { // 剪枝 if (root == null || root.val == 3) { return; } // 尝试 path.Add(root); if (root.val == 7) { // 记录解 res.Add(new List图 13-3 根据约束条件剪枝
## 13.1.3 框架代码 接下来,我们尝试将回溯的“尝试、回退、剪枝”的主体框架提炼出来,提升代码的通用性。 在以下框架代码中,`state` 表示问题的当前状态,`choices` 表示当前状态下可以做出的选择: === "Python" ```python title="" def backtrack(state: State, choices: list[choice], res: list[state]): """回溯算法框架""" # 判断是否为解 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) ``` === "C++" ```cpp title="" /* 回溯算法框架 */ void backtrack(State *state, vector图 13-4 保留与删除 return 的搜索过程对比
相比基于前序遍历的代码实现,基于回溯算法框架的代码实现虽然显得啰唆,但通用性更好。实际上,**许多回溯问题可以在该框架下解决**。我们只需根据具体问题来定义 `state` 和 `choices` ,并实现框架中的各个方法即可。 ## 13.1.4 常用术语 为了更清晰地分析算法问题,我们总结一下回溯算法中常用术语的含义,并对照例题三给出对应示例,如表 13-1 所示。表 13-1 常见的回溯算法术语