Перебор с возвратом
Перебор с возвратом — способ систематически проверить варианты, не повторяя уже пройденные ветви. Алгоритм строит дерево нұсқа, сразу отбрасывает невозможные продолжения и возвращается к последнему выбору, если решение не найдено.
Идея перебора с возвратом
В задаче есть состояние и несколько допустимых действий. Каждое действие создаёт новый узел дерева. Из него снова выполняется выбор, пока не будет достигнуто конечное состояние — готовое решение или доказанная невозможность продолжения.
Перебор с возвратом (бэктрекинг, или бектрекинг) — алгоритм, который рекурсивно перебирает варианты, проверяет ограничения и отменяет последний выбор, если он больше не может привести к решению.
В отличие от полного перебора, бэктрекинг не обязан строить всё дерево. Если частичное решение уже нарушает ограничение, вся его ветвь отсекается. Поэтому важна проверка не только готового ответа, но и каждого промежуточного состояния.
Если на каждом шаге рассмотрены все допустимые действия, а ветвь отсекается только после доказанного нарушения ограничения, перебор с возвратом найдёт решение, если оно существует, и не пропустит ни одного допустимого решения.
Модель состояния и дерева
Перед записью алгоритма нужно описать состояние: что уже выбрано, какие элементы использованы и какие ограничения должны выполняться. Узел дерева хранит частичное решение, ребро — добавленный вариант, глубина — число сделанных выборов.
- Выбрать позицию, которую нужно заполнить.
- Перебрать кандидатов для этой позиции.
- Тексеру, не нарушает ли кандидат ограничения.
- Добавить кандидат к текущему состоянию.
- Перейти к следующей позиции.
- После возврата удалить кандидат и попробовать следующий.
Здесь \(b\) — среднее число нұсқа на шаге, а \(d\) — глубина. В задачах на перестановки ветвление уменьшается: сначала доступно \(n\) элементов, затем \(n-1\) и так далее. Поэтому число листьев равно \(n!\).
Шаблон алгоритма
Обычно бэктрекинг удобно оформлять процедурой search, получающей текущее состояние. Базовый случай проверяет, заполнено ли решение. Рекурсивная часть перебирает кандидатов, временно изменяет состояние, вызывает себя и обязательно отменяет изменение.
search(state): if state is complete: process(state) return for candidate in candidates(state): if satisfies_constraints(state, candidate): add(candidate, state) search(state) remove(candidate, state)
После завершения вызова search(state) состояние должно быть таким же, каким оно было до вызова. Поэтому каждый добавленный элемент должен быть удалён после рекурсивного шага — даже если найдено решение или ветвь оказалась неудачной.
Если требуется найти только одно решение, после успешного рекурсивного вызова можно немедленно завершить все уровни. Если нужно посчитать или вывести все решения, поиск продолжается после каждого найденного ответа.
Зачем после рекурсивного вызова удалять добавленный элемент?
Разобранный пример: размещение ладей
Нужно расставить \(n\) ладей на доске \(n \times n\) так, чтобы никакие две не находились в одной строке или столбце. Разберём случай \(n=3\). Будем ставить по одной ладье в каждую строку, поэтому ограничение на строки выполняется автоматически. Проверяем только занятость столбцов.
Состояние можно записать Тізіммен столбцов: \([2,1]\) означает, что в первой строке ладья стоит в столбце 2, во второй — в столбце 1. Множество занятых столбцов нужно хранить для быстрой проверки.
Показать Шешім Жауап
Все шешімдер задаются перестановками столбцов: \([1,2,3]\), \([1,3,2]\), \([2,1,3]\), \([2,3,1]\), \([3,1,2]\), \([3,2,1]\). В каждой ветви следующий столбец выбирается только из ещё не занятых.
def search(row, used, placement): if row == 3: print(placement) return for col in range(3): if col not in used: used.add(col) placement.append(col + 1) search(row + 1, used, placement) placement.pop() used.remove(col) search(0, set(), [])
В более сложных задачах ограничение проверяют по нескольким признакам: сумме выбранных чисел, соседним элементам, диагоналям, длине пути или переходам между состояниями. Если состояние удобно представить явно, полезны конечный автомат и таблица допустимых переходов между состояниями.
Отсечение ветвей и оценка эффективности
Чем раньше обнаружено нарушение, тем меньше лишних вызовов. Простое отсечение проверяет уже выбранные элементы. Более сильное отсечение оценивает, можно ли достичь цели даже при самом благоприятном продолжении. Если нельзя, ветвь закрывается сразу.
Проверяйте наиболее строгие ограничения раньше остальных, храните занятые ресурсы в множествах или масках, а кандидатов перебирайте в порядке, который быстрее приводит к противоречию. Подробные приёмы приведены на странице отсечения ветвей перебора.
В худшем случае время остаётся экспоненциальным или факториальным. Однако оценка зависит от числа реально посещённых узлов, а не только от размера полного дерева. Память обычно равна глубине рекурсии плюс данным состояния: \(O(d)\) при изменении состояния на месте.
1. Забывают отменить выбор после рекурсии. 2. Проверяют ограничение только на листе и получают огромный лишний перебор. 3. Допускают один и тот же элемент повторно, хотя нужны различные элементы. 4. Путают индекс позиции со значением элемента. 5. Не останавливают поиск после первого ответа или, наоборот, останавливают его, когда нужны все решения. 6. Неверно задают базовый случай: глубина должна означать полностью построенное решение.
Для отладки полезно печатать глубину, выбранный кандидат и причину отсечения. Разбор таких ошибок связан с отладкой алгоритма, а число вызовов удобно изучать через дерево рекурсивных вызовов. Не забывайте о корректном базовом случае рекурсии.
Как решать экзаменационные тапсырма
- Определите, что является одним шагом выбора и что хранит состояние.
- Запишите ограничения для частичного шешімдер.
- Найдите базовый случай: когда Шешім готово или тапсырма невозможна.
- Проверьте, что после возврата состояние восстановлено.
- Если спрашивается число нұсқа, не прекращайте поиск после первого ответа.
- Сделайте маленькое дерево вручную и сравните с программой.
При построении решения отделяйте входные данные от результата: это помогает не перепутать, что перебирается, а что вычисляется. Смотрите также страницу входные и выходные данные алгоритма и материал о построении алгоритма.
Быстрая проверка
Главное
- Перебор с возвратом строит дерево нұсқа и отменяет последний выбор при неудаче.
- Каждый шаг должен проверять ограничения частичного шешімдер, а не только готового ответа.
- После рекурсивного вызова состояние обязательно восстанавливают.
- Базовый случай определяет готовое Шешім; режим поиска — одно Шешім или все шешімдер.
- Раннее отсечение уменьшает число узлов, но не всегда меняет худшую оценку сложности.