圖 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 常見的回溯演算法術語