7. Переборные задачи
На практике неоднократно встречаются задачи, где требуется обработать (перебрать) все элементы некоторого конечного множества. В простейших случаях элемент множества можно представить числом. Пример такой задачи — поиск заданного числа в одномерном массиве. Здесь для указания элемента массива достаточно задать его индекс.
Более сложные случаи, когда элементы интересующего нас множества сами имеют некоторую структуру. (Именно такие задачи обычно относят к переборным.) Во многих случаях элементы множества естественно записывать в виде последовательности чисел фиксированного или переменного размера. Очень многие переборные задачи можно свести к перебору последовательностей.
В некоторых задачах (см. например, задачу о коммивояжере в главе 1) требуется генерировать последовательности путем всевозможных перестановок компонент некоторой заданной последовательности фиксированной длины. Правила генерации последовательностей могут меняться в зависимости от задачи:
случайным образом,
в лексикографическом порядке,
путем различных сочетаний компонент
и т. п.
Примером такой задачи (в действительности она эквивалентна задаче перебора последовательностей) является задача прохождения (обхода) деревьев. Дерево — это граф без циклов, в котором выделена одна вершина, называемая корнем. Для каждой вершины дерева существует единственный путь в нее из корня. Задача состоит в том, чтобы «обойти» все вершины дерева. Если перенумеровать все ребра, выходящие из каждой вершины, то путь из корня, однозначно сопоставляемый каждой вершине, можно будет в свою очередь задать с помощью последовательности номеров проходимых ребер. Лексикографический порядок перебора последовательностей отвечает способу прохождения дерева, известному как перебор в глубину. Находясь в какой-либо вершине, мы сначала пытаемся пойти «в глубину», т. е. удалиться от корня; если это невозможно, то мы возвращаемся назад и пытаемся пойти в глубину по следующему ребру.
Многие практические задачи сводятся к отысканию минимума (максимума) некоторой функции на множестве. Искомым элементом множества часто является функция от времени (например, ищется закон управления самолетом, обеспечивающий наибыстрейшее достижение заданной высоты и скорости). Эту функцию аппроксимируют последовательностью ее значений в дискретные моменты времени, получая задачу поиска минимума в множестве последовательностей. Можно осуществить поиск минимума путем полного перебора всех допустимых последовательностей. Метод динамического программирования позволяет в ряде случаев сократить перебор. Рассмотрим несколько типичных примеров генерации последовательностей и работы с ними при решении переборных задач.
7.1. Полный перебор.
Во многих прикладных задачах требуется найти оптимальное решение среди очень большого (но конечного!) числа вариантов. Иногда удается построить это решение сразу, но в большинстве случаев единственный способ его отыскать состоит в переборе всех возможных вариантов и сравнении их между собой.
Для решения переборных задач обычно используются разновидности следующей схемы программы:
Схема 1
Var x :элемент множества;
x := первый элемент;
обработать x; While x<> последний элемент Do Begin x: =следующий за x;
обработать x;
End;
Иногда удается добавить к множеству еще один, фиктивный элемент, не подлежащий обработке, который располагается перед первым обрабатываемым элементом, либо вслед за последним В этом случае обработку первого элемента можно выполнить наряду со всеми в цикле:
Схема 2 Схема 3
x:=фиктивный элемент; x:=первый элемент;
Repeat Repeat
x:=следующий за x; обработать x;
обработать x; x:=следующий за x;
Until x = последний элемент; Until x = фиктивный элемент;
Для конкретизации этих схем необходимо ответить на два вопроса.