Рисунок 13-1 Поиск узлов при прямом обходе
## 13.1.1 Попытка и откат **Алгоритм называется поиском с возвратом, потому что при поиске в пространстве решений он использует стратегию «попытка» и «откат»**. Когда в процессе поиска алгоритм приходит в состояние, из которого нельзя двигаться дальше или нельзя получить удовлетворяющее условиям решение, он отменяет предыдущий выбор, возвращается к более раннему состоянию и пробует другие возможные варианты. Для примера 1 посещение каждого узла представляет собой «попытку», а прохождение листового узла или возврат к родителю через `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 Обрезка Сложные задачи поиска с возвратом обычно содержат одно или несколько ограничений, **которые часто можно использовать для «обрезки»**. !!! question "Пример 3" Найдите в двоичном дереве все узлы со значением $7$ , верните пути от корня до этих узлов, **причем путь не должен содержать узлы со значением $3$**. Чтобы выполнить это ограничение, **нам нужно добавить операцию обрезки**: во время поиска, если встречается узел со значением $3$ , мы сразу возвращаемся и не продолжаем дальнейший поиск. Код выглядит так: === "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