図 13-1 前順走査でノードを探索する
## 13.1.1 試行と戻る **バックトラッキングアルゴリズムと呼ばれるのは、解空間を探索する際に「試行」と「戻る」という戦略を取るためです**。探索中に、ある状態から先へ進めない、または条件を満たす解を得られないと分かった場合、アルゴリズムは直前の選択を取り消して前の状態へ戻り、別の選択肢を試します。 例題1では、各ノードへの訪問が 1 回の「試行」に対応し、葉ノードを越えるか親ノードへ戻る `return` は「戻る」を表します。 ここで強調しておきたいのは、**戻るとは関数の return だけを指すわけではない**という点です。これを説明するために、例題1を少し拡張します。 !!! question "例題2" 二分木の中で値が $7$ のノードをすべて探索し、**根ノードからそれらのノードまでの経路を返してください**。 例題1のコードを土台に、訪問済みノードの経路を記録するためのリスト `path` を導入します。値が $7$ のノードに到達したら、`path` をコピーして結果リスト `res` に追加します。走査が完了すると、`res` にはすべての解が保存されています。コードは次のとおりです。 === "Python" ```python title="preorder_traversal_ii_compact.py" def pre_order(root: TreeNode): """前順走査:例題 2""" 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" /* 前順走査:例題 2 */ 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" /* 前順走査:例題 2 */ 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" /* 前順走査:例題 2 */ void PreOrder(TreeNode? root) { if (root == null) { return; } // 試す path.Add(root); if (root.val == 7) { // 解を記録 res.Add(new List図 13-2 試行と戻る
## 13.1.2 枝刈り 複雑なバックトラッキング問題には、通常 1 つ以上の制約条件が含まれます。**制約条件は多くの場合「枝刈り」に利用できます**。 !!! question "例題3" 二分木の中で値が $7$ のノードをすべて探索し、根ノードからそれらのノードまでの経路を返してください。**ただし、経路には値が $3$ のノードを含めてはいけません**。 上の制約条件を満たすために、**枝刈り操作を追加する必要があります**。探索中に値が $3$ のノードに出会った場合は、そこで早めに return し、それ以上探索を続けません。コードは次のとおりです。 === "Python" ```python title="preorder_traversal_iii_compact.py" def pre_order(root: TreeNode): """前順走査:例題 3""" # 枝刈り 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" /* 前順走査:例題 3 */ 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" /* 前順走査:例題 3 */ 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" /* 前順走査:例題 3 */ 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