Поиск в глубину прост, его легко программировать и он в некоторых случаях хорошо работает. Программа для решения задачи о восьми ферзях (см. гл. 4) фактически была примером поиска в глубину. Для того, чтобы можно было применить к этой задаче описанную выше процедуру решить
, необходимо сформулировать задачу в терминах пространства состояний. Это можно сделать так:
• вершины пространства состояний — позиции, в которых поставлено 0 или более ферзей на нескольких последовательно расположенных горизонтальных линиях доски;
• вершина-преемник данной вершины может быть получена из нее после того, как в соответствующей позиции на следующую горизонтальную линию доски будет поставлен еще один ферзь, причем таким образом, чтобы ни один из уже поставленных ферзей не оказался под боем;
• стартовая вершина — пустая доска (представляется пустым списком);
• целевая вершина — любая позиция с восемью ферзями (правило получения вершины-преемника гарантирует, что ферзи не бьют друг друга).
Позицию на доске будем представлять как список Y-координат поставленных ферзей. Получаем программу:
после( Ферзи, [Ферзь | Ферзи] ) :-
принадлежит( Ферзь, [1, 2, 3, 4, 5, 6, 7, 8] ),
% Поместить ферзя на любую вертикальную линию
небьет( Ферзь, Ферзи).
цель( [ _, _, _, _, _, _, _, _ ] )
% Позиция с восемью ферзями
Отношение небьет
означает, что Ферзь
не может поразить ни одного ферзя из списка Ферзи
. Эту процедуру можно легко запрограммировать так же, как это сделано в гл. 4. Ответ на вопрос
?- решить( [], Решение)
будет выглядеть как список позиций с постепенно увеличивающимся количеством поставленных ферзей. Список завершается "безопасной" конфигурацией из восьми ферзей. Механизм возвратов позволит получить и другие решения задачи.
Поиск в глубину часто работает хорошо, как в рассмотренном примере, однако наша простая процедура решить
может попасть в затруднительное положение, причем многими способами. Случится ли это или нет — зависит от структуры пространства состояний. Для того, чтобы затруднить работу процедуры решить в примере рис. 11.4, достаточно внести в задачу совсем небольшое изменение: добавить дугу, ведущую из
Рис. 11.5. Начинаясь в
Очевидное усовершенствование нашей программы поиска в глубину — добавление к ней механизма обнаружения циклов. Ни одну из вершин, уже содержащихся в пути, построенном из стартовой вершины в текущую вершину, не следует вторично рассматривать в качестве возможной альтернативы продолжения поиска. Это правило можно сформулировать в виде отношения
вглубину( Путь, Верш, Решение)
Как видно из рис. 11.6, Верш
— это состояние, из которого необходимо найти путь до цели; Путь
— путь (список вершин) между стартовой вершиной и Верш
; Решение
— Путь
, продолженный до целевой вершины.
Рис. 11.6. Отношение вглубину( Путь, В, Решение)
.
Для облегчения программирования вершины в списках, представляющих пути, будут расставляться в обратном порядке. Аргумент Путь
нужен для того,
(1) чтобы не рассматривать тех преемников вершины Верш
, которые уже встречались раньше (обнаружение циклов);
(2) чтобы облегчить построение решающего пути Решение
. Соответствующая программа поиска в глубину показана на рис. 11.7.
решить( Верш, Решение) :-
вглубину( [], Верш, Решение).
вглубину( Путь, Верш, [Верш | Путь] ) :-
цель( Верш).
вглубину( Путь, Верш, Реш) :-
после( Верш, Верш1),
not принадлежит( Верш1, Путь), % Цикл?
вглубину( [Верш | Путь], Верш1, Реш).
Рис. 11.7. Программа поиска в глубину без зацикливания.
Теперь наметим один вариант этой программы. Аргументы Путь
и Верш
процедуры вглубину
можно объединить в один список [Верш | Путь]
. Тогда, вместо вершины-кандидата Верш
, претендующей на то, что она находится на пути, ведущем к цели, мы будем иметь П = [Верш | Путь]
, который претендует на то, что его можно продолжить вплоть до целевой вершины. Программирование соответствующего предиката
вглубину( П, Решение)