Перебор с возвратом Баронов Антон Введение Во многих задачах из различных областей знания ставятся вопросы-задания типа: “Сколько существует способов …”, “Подсчитайте количество элементов …”, “Перечислите все возможные варианты …”, “Есть ли способ …”, “Существует ли объект…” и т. п. Ответы на них, как правило, требуют исчерпывающего поиска в некотором множестве M всех возможных вариантов, среди которых находятся решения конкретной задачи. Один из общих методов организации исчерпывающего поиска: перебор с возвратом (backtracking). Введение Решение задачи методом перебора с возвратом строится конструктивно последовательным расширением частичного решения. Если на конкретном шаге такое расширение провести не удается, то происходит возврат к более короткому частичному решению, и попытки его расширить продолжаются. Для ускорения перебора с возвратом вычисления всегда стараются организовать так, чтобы была возможность отметать как можно раньше и как можно больше заведомо неподходящих вариантов M. Перебор с возвратом также можно рассмотреть как задачу об обходе дерева решений. Вычислительная схема Сформулируем задачу обхода произвольного дерева. Будем считать, что у нас имеется Робот, который в каждый момент находится в одной из вершин дерева (вершины изображены на рисунке кружочками). Он умеет выполнять команды: вверх_налево (идти по самой левой из выходящих вверх стрелок) вправо (перейти в соседнюю справа вершину) вниз (спуститься вниз на один уровень) и проверки, соответствующие возможности выполнить каждую из команд, называемые "есть_сверху" "есть_справа" "есть_снизу" Вычислительная схема У Робота есть команда "обработать" и его задача - обработать все листья (вершины, из которых нет стрелок вверх, то есть где условие "есть_сверху" ложно). Нам понадобится такая процедура: void вверх_до_упора_и_обработать | //{дано: (ОЛ), надо: (ОЛН)} { | //{инвариант: ОЛ} | while( есть_сверху ){ | | вверх_налево (); |} | //{ОЛ, Робот в листе} | обработать(); | //{ОЛН} } Основной алгоритм: //дано: Робот в корне, листья не обработаны //надо: Робот в корне, листья обработаны //{ОЛ} вверх_до_упора_и_обработать (); //{инвариант: ОЛН} While(есть_снизу){ | if(есть_справа){ | |//{ОЛН, есть справа} | | вправо(); | | //{ОЛ} | | вверх_до_упора_и_обработать(); | }else{ | | //{ОЛН, не есть_справа, есть_снизу} | | вниз(); |} } //{ОЛН, Робот в корне => все листья обработаны} Вычислительная схема (1) (2) (3) (4) Осталось воспользоваться следующими свойствами команд Робота (сверху записаны условия, в которых выполняется команда, снизу - утверждения о результате ее выполнения): {ОЛ, не есть_сверху} обработать {ОЛН} {ОЛ} вверх_налево {ОЛ} {есть_справа, ОЛН} вправо {ОЛ} {не есть_справа, ОЛН} вниз {ОЛН} Пример – задача о ферзях Рассмотрим классическую задачу о восьми ферзях. Как известно, в шахматах ферзи атакуют друг друга по горизонтали, по вертикали или по диагоналям шахматной доски. Задача состоит в том, чтобы найти такое размещение 8 ферзей на доске 8х8, чтобы никакие два ферзя не атаковали друг друга. Решение этой задачи легко находится с помощью перебора с возвратом. Приведем основной фрагмент реализованного на c++ алгоритма решения этой задачи. Пример – задача о ферзях bool place_queens(Queenboard& qb, int col) { bool inserted = false; for (int row = 0; row < BOARDSIZE; row++) { if (! qb.is_space_under_attack(row, col)) { // ставим ферзя qb.occupy_space(row, col); inserted = true; if (col == BOARDSIZE - 1) { // найдено решение! cout << qb << "\n"; return true; } else { // ставим ферзя в следующую колонку if (place_queens(qb, col + 1)) { return true; } else { inserted = false; } } } } if (! inserted) { // возвращаемся к предыдущей колонке qb.clear_column(col - 1); return false; } } Алгоритм проходит по всему дереву решений (возможных расстановок ферзей) и, отбрасывая неверные, находит правильные решения. Заключение Перебор с возвратом – мощный инструмент для быстрого нахождения алгоритмов решения разного рода задач, однако нельзя не отметить, что для многих из них существуют более эффективные способы решения по сравнению с приведенным. Однако дидактическая ценность метода перебора с возвратом в соединении с рекурсией неоспорима. Во-первых, программы решения многих задач строятся по единой схеме, а во-вторых, они компактны и тем самым просты для понимания и усвоения соответствующих идей.