?) {
// Проверка, является ли состояние решением
if (isSolution(state)) {
// Запись решения
recordSolution(state, res)
// Не продолжать поиск
return
}
// Перебор всех вариантов
for (choice in choices) {
// Обрезка: проверка легитимности выбора
if (isValid(state, choice)) {
// Попытка: сделать выбор, обновить состояние
makeChoice(state, choice)
backtrack(state, choices, res)
// Возврат: отмена выбора, возврат к предыдущему состоянию
undoChoice(state, choice)
}
}
}
```
=== "Ruby"
```ruby title=""
### Каркас алгоритма поиска с возвратом ###
def backtrack(state, choices, res)
# Проверка, является ли состояние решением
if is_solution?(state)
# Запись решения
record_solution(state, res)
return
end
# Перебор всех вариантов
for choice in choices
# Обрезка: проверка легитимности выбора
if is_valid?(state, choice)
# Попытка: сделать выбор, обновить состояние
make_choice(state, choice)
backtrack(state, choices, res)
# Возврат: отмена выбора, возврат к предыдущему состоянию
undo_choice(state, choice)
end
end
end
```
Теперь на основе каркаса кода решим пример 3. Состояние `state` -- это путь обхода узлов, выбор `choices` -- это левый и правый дочерние узлы текущего узла, результат `res` -- список путей:
```src
[file]{preorder_traversal_iii_template}-[class]{}-[func]{backtrack}
```
Согласно условию задачи после нахождения узла со значением $7$ необходимо продолжать поиск, **поэтому следует удалить оператор `return` после записи решения**. На следующем рисунке сравнивается процесс поиска с сохранением и удалением оператора `return`.

По сравнению с реализацией на основе предварительного обхода, реализация на основе каркаса поиска с возвратом выглядит более громоздкой, но обладает большей универсальностью. На самом деле **многие задачи поиска с возвратом можно решить в рамках этого каркаса**. Необходимо лишь определить `state` и `choices` в соответствии с конкретной задачей и реализовать методы каркаса.
## Основные термины
Для более четкого понимания алгоритмических задач мы систематизируем значения часто используемых терминов обратного поиска и приведем соответствующие примеры для задачи 3, как показано в следующей таблице.
Таблица Основные термины обратного поиска
| Термин | Определение | Пример 3 |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Решение (solution) | Ответ, удовлетворяющий определенным условиям задачи, может быть одно или несколько решений | Все пути от корневого узла до узла $7$, удовлетворяющие условиям |
| Ограничение (constraint) | Ограничение на допустимость решения, обычно используется для обрезки | Путь не содержит узлов со значением $3$ |
| Состояние (state) | Ситуация задачи в определенный момент, включая сделанные выборы | Текущий посещенный путь узлов, т. е. список узлов `path` |
| Попытка (attempt) | Процесс исследования пространства решений на основе доступных выборов, включая выбор, обновление состояния, проверку на решение | Рекурсивный доступ к левому (правому) дочернему узлу, добавление узла в `path`, проверка значения узла на равенство $7$ |
| Возврат (backtracking) | Отмена предыдущих выборов и возврат к предыдущему состоянию при встрече состояния, не удовлетворяющего ограничению | При переходе через листовой узел, завершении посещения узла, встрече узла со значением $3$ поиск прекращается, происходит выход из функции |
| Обрезка (pruning) | Метод избежания бессмысленных путей поиска на основе особенностей задачи и ограничений, может повысить эффективность поиска | При встрече узла со значением $3$ не продолжать поиск |
!!! tip
Понятия задачи, решения, состояния и т. д. являются универсальными и встречаются в алгоритмах «разделяй и властвуй», поиска с возвратом, динамического программирования, жадных алгоритмах и других.
## Преимущества и ограничения
Алгоритм поиска с возвратом по своей сути является алгоритмом поиска в глубину, который пытается найти все возможные решения до тех пор, пока не будет найдено удовлетворяющее условиям решение. Преимущество этого метода заключается в том, что он может найти все возможные решения, и при разумной обрезке обладает высокой эффективностью.
Однако при обработке крупномасштабных или сложных задач **эффективность работы алгоритма поиска с возвратом может быть неприемлемой**.
- **Время**: Алгоритм поиска с возвратом обычно требует обхода всех возможных состояний пространства состояний, временная сложность может достигать экспоненциального или факториального порядка.
- **Пространство**: При рекурсивных вызовах необходимо сохранять текущее состояние (например, путь, вспомогательные переменные для обрезки и т. д.), когда глубина очень велика, требования к пространству могут стать очень большими.
Несмотря на это, **алгоритм поиска с возвратом остается лучшим решением для некоторых задач поиска и задач удовлетворения ограничений**. Для этих задач, поскольку невозможно предсказать, какие выборы приведут к правильному решению, необходимо перебрать все возможные варианты. В этом случае **ключевым является оптимизация эффективности**, обычно используются два метода оптимизации эффективности.
- **Обрезка**: Избежание поиска путей, которые определенно не приведут к решению, тем самым экономя время и пространство.
- **Эвристический поиск**: Введение некоторых стратегий или оценочных значений в процессе поиска для приоритетного поиска путей, которые с наибольшей вероятностью приведут к правильному решению.
## Типичные примеры задач поиска с возвратом
Алгоритм поиска с возвратом может использоваться для решения многих задач поиска, задач удовлетворения ограничений и задач комбинаторной оптимизации.
**Задачи поиска**: Цель этих задач -- найти решение, удовлетворяющее определенным условиям.
- Задача о полной перестановке: Дано множество, найти все возможные перестановки и комбинации.
- Задача о сумме подмножества: Дано множество и целевая сумма, найти все подмножества множества, сумма которых равна целевой сумме.
- Задача о Ханойской башне: Даны три стержня и серия дисков разного размера, требуется переместить все диски с одного стержня на другой, перемещая за раз только один диск, и нельзя класть большой диск на маленький.
**Задачи удовлетворения ограничений**: Цель этих задач -- найти решение, удовлетворяющее всем ограничениям.
- $n$ ферзей: На шахматной доске $n \times n$ разместить $n$ ферзей так, чтобы они не атаковали друг друга.
- Судоку: В сетке $9 \times 9$ заполнить числа от $1$ до $9$ так, чтобы в каждой строке, каждом столбце и каждой подсетке $3 \times 3$ числа не повторялись.
- Задача о раскраске графа: Дан неориентированный граф, раскрасить каждую вершину графа минимальным количеством цветов так, чтобы смежные вершины имели разные цвета.
**Задачи комбинаторной оптимизации**: Цель этих задач -- найти оптимальное решение в комбинаторном пространстве, удовлетворяющее определенным условиям.
- Задача о рюкзаке 0-1: Дан набор предметов и рюкзак, каждый предмет имеет определенную ценность и вес, требуется выбрать предметы в пределах ограничения вместимости рюкзака так, чтобы общая ценность была максимальной.
- Задача коммивояжера: В графе, начиная с одной точки, посетить все остальные точки ровно один раз и вернуться в начальную точку, найти кратчайший путь.
- Задача о максимальной клике: Дан неориентированный граф, найти максимальный полный подграф, т. е. подграф, в котором между любыми двумя вершинами есть ребро.
Обратите внимание, что для многих задач комбинаторной оптимизации поиск с возвратом не является оптимальным решением.
- Задача о рюкзаке 0-1 обычно решается с помощью динамического программирования для достижения более высокой временной эффективности.
- Задача коммивояжера -- это известная NP-трудная задача, обычно используются генетические алгоритмы, алгоритмы муравьиной колонии и другие методы решения.
- Задача о максимальной клике -- это классическая задача теории графов, может быть решена с помощью жадных алгоритмов и других эвристических методов.