|
Часть 7.Выталкивание (или удаление) последнего элемента (верхушки) из стека. Просмотр последнего элемента (верхушки) без его удаления. Проверка наличия данного элемента в стеке. Добавление списка элементов в стек. Операции 5 и 6 можно определить на основании первых четырех операций. Теперь опишем эти операции на языке PROLOG, используя в качестве строительных блоков списки. 1. empty_stack ( [ ] ). Этот предикат можно использовать либо для проверки пустоты стека, либо для создания нового пустого стека. 2 - 4. stack (Top, Stack, [Top | Stack] ). Этот предикат выполняет операции выталкивания, добавления и считывания последнего элемента стека в зависимости от связанных переменных, передаваемых ему в качестве параметров. Например, если первые два аргумента представляют собой связанные переменные, то в третьем аргументе формируется новый стек. Аналогично, если третий элемент связан со стеком, можно получить значение верхушки стека. Тогда второй аргумент будет связан с новым стеком, из которого удален последний элемент. И, наконец, если передать стек в качестве третьего аргумента, то через первый элемент можно получить значение его верхушки. 5. member_stack(Element, Stack) :- member(Element, Stack). Это выражение позволяет определить, содержится ли данный элемент в стеке. Естественно, этот же результат можно получить с помощью рекурсивного вызова, просматривая следующий элемент стека, а затем, если этот элемент не соответствует значению аргумента Element, выталкивая его из стека. Эту процедуру необходимо выполнять до тех пор, пока предикат проверки пустоты стека не примет значение "истина". 6.add_list_to_stack(List, Stack, Result) :- append(List, Stack, Result). Значение первого аргумента List добавляется к значению второго аргумента Stack для получения нового стека Result. Эту же операцию можно выполнить, выталкивая элементы из стека List и добавляя каждый следующий элемент во временный стек до тех пор, пока стек List не окажется пустым. Затем нужно выталкивать по очереди элементы из временного стека и добавлять их в стек Stack до опорожнения временного стека. Предикат append подробно описан в разделе 14.8. Осталось определить предикат reverse_print_stack, выводящий стек в обратном порядке. Это очень полезно, если в стеке в обратном порядке хранится текущий путь от начального состояния к текущему состоянию графа поиска. Несколько примеров использования этого предиката будут приведены в следующих подразделах. reverse_print_stack(S) :- empty_stack(S). reverse_print_stack(S) :- stack(E, Rest, S) , reverse_print_stack(Rest) , write(E), nl. Глава 14. Введение в PROLOG 623 14.2.2. Очередь Очередь (queue) - это структура данных FIFO (First-In-First-Out) - "первым вошел, первым вышел". Ее зачастую рассматривают как список, в котором элементы удаляются из одного конца, а добавляются в другой. Очередь использовалась для определения поиска в ширину в главах 3 и 4. Для реализации очереди требуются следующие операции. empty_queue ([ ] ). Этот предикат либо выполняет проверку очереди на пусто ту, либо инициализирует новую пустую очередь. enqueue (Е, [ ], [Е]). enqueue(E, [H|T], [H|Tnew]) :- enqueue(E, T, Tnew). Этот рекурсивный предикат добавляет в очередь, определяемую вторым аргументом, элемент Е. Третий элемент представляет новую очередь. dequeue (Е, [Е | Т] , Т). Этот предикат создает новую очередь (третий аргумент), которая является результатом удаления очередного элемента (первого аргумента) из исходной очереди, определяемой вторым аргументом. dequeue (Е, [Е | Т] , _). Этот предикат позволяет прочитать следующий элемент Е данной очереди. member_queue(Element, Queue) :- member(Element, Queue). Это выражение проверяет, содержится ли элемент Element в очереди Queue. add_list_to_queue(List, Queue, Newqueue) :- append(Queue,List, New-queue). Очистка всех элементов очереди. Очевидно, что операции 5 и 6 можно реализовать на основе первых четырех операций. Предикат append описан в разделе 14.10. 14.2.3. Приоритетная очередь В приоритетной очереди (priority queue) элементы обычной очереди упорядочены, и каждый новый элемент добавляется в соответствующее место. Оператор удаления элемента из очереди извлекает из нее лучший отсортированный элемент. Приоритетная очередь была использована при разработке "жадного" алгоритма поиска в главе 4. Поскольку приоритетная очередь - это, по существу, отсортированная обычная очередь, многие из ее операций совпадают с операциями для обычной очереди. К таким операциям относятся empty_queue, member_queue и dequeue (следующим элементом для операции dequeue является "лучший" отсортированный элемент). Операции enqueue в приоритетной очереди соответствует операция insert_pq, поскольку каждый новый элемент должен помещаться в отведенное для него место. insert_pq(State, [ ], [State]):- !. insert_pq(State, [H|Tail], [State, H|Tail]):- precedes(State, H). insert_pq(State, [H|T], [H|Tnew]):- insert_pq(State, T, Tnew). precedes(X, Y) :- X < Y. %оператор сравнения зависит от типов %элементов Первый аргумент такого предиката - это вновь добавляемый элемент. Вторым аргументом является существующая приоритетная очередь, а третьим - расширенная очередь. Предикат precedes отвечает за соблюдение порядка элементов в очереди. 624 Часть VI. Языки и технологии программирования для искусственного интеллекта Следующим оператором для приоритетной очереди является insert_list_pq. Этот предикат используется для добавления несортированного списка или множества элементов в приоритетную очередь. Это необходимо при добавлении дочернего состояния в приоритетную очередь при реализации "жадного" алгоритма поиска (глава 4 и подраздел 14.4.3). Оператор insert_list_pq использует оператор insert_pq для добавления в приоритетную очередь каждого нового элемента. insert_list_pq([ ], L, L). insert_list_pq([State/Tail], L, New_L) :- insert_pq(State, L, L2), insert_list_pq(Tail, L2, New_L). 14.2.4. Множество И, наконец, опишем абстрактный тип данных множество (set). Множество - это набор неповторяющихся элементов. Его можно использовать для объединения всех дочерних состояний или поддержания списка closed в алгоритме поиска, представленном в главах 3 и 4. Множество элементов, например [а, Ь], представляется в виде списка [а,b], в котором порядок элементов не играет роли. Для множества необходимо определить операции empty_set, member_set, delete_if_in_set и add_if_not_in_set. Потребуются также операции для объединения и сравнения множеств, включая union, intersection, set_difference,subset и equal_set. empty_set( [ ]). member_set(E, S) :- member(E, S). delete_if_in_set(E, [ ], [ ]). delete_if_in_set(E, [E|T], T) :- !. delete_if_in_set(E, [H|T], [H|T_new]) :- delete_if_in_set(E, T, T_new), !. add_if_not_in_set(X, S, S) :- member(X, S), !. add_if_not_in_set(X, S, [X|S]). union([ ], S, S). union([H|T], S, S_new):- union(T, S, S2), add_if_not_in_set(H, S2, S_new), !. subset([ ], _). subset([H|T], S) :- member_set(H, S) , subset(T, S). intersection([ ], _, [ ]). intersection([H|T], S, [H|S_new]) :- member_set(H, S), intersection(T, S, S_new), !. intersection([_|T], S, S_new) :- intersection(T, S, S_new), !. set_difference([ ], _, [ ]). set_difference([H|T], S, T_new) :- member_set(H, S), set_difference(T, S, T_new), !. set_difference([H|T], S, [H|T_new]) :- set_difference(T, S, T_new), !. Глава 14. Введение в PROLOG 625 equal_set(Sl, S2) subset(SI, S2) subset(S2, SI) 14.3. Пример продукционной системы на языке PROLOG В этом разделе будет рассмотрена продукционная система для решения задачи перевозки человека, волка, козы и капусты. Эта задача формулируется следующим образом. Человеку требуется переправиться через реку и перевезти с собой волка, козу и капусту. На берегу реки находится лодка, которой должен управлять человек. Лодка одновременно может перевозить не более двух пассажиров (включая лодочника). Если волк останется на берегу с козой, то он ее съест. Если коза останется на берегу с капустой, то она уничтожит капусту. Требуется выработать последовательность переправки через реку таким образом, чтобы все четыре пассажира были доставлены в целости и сохранности на другой берег реки. Рассмотрим продукционную систему, обеспечивающую решение этой проблемы. Вопервых, очевидно, что эту задачу можно представить как задачу поиска на графе. Для этого нужно учесть все возможные ходы, доступные в каждый момент решения задачи. Некоторые из этих ходов могут оказаться неудовлетворительными, поскольку приводят к получению неустойчивых состояний (кто-то кого-то съест). Для начала предположим, что все состояния устойчивы, и просто рассмотрим граф возможных состояний. В лодке можно перевозить четыре комбинации пассажиров: человека и волка, человека и козу, человека и капусту, а также одного человека. "Состоянием мира" является некоторая комбинация символов на двух берегах. На рис. 14.1 показаны несколько состояний процесса поиска. "Состояние мира" можно представить с помощью предиката state (F, W, G, С), в котором первый параметр определяет местоположение человека, второй параметр - местоположение волка, третий - козы, а четвертый - капусты. Предполагается, что река течет с севера на юг, поэтому берега обозначим символами е (восточный) и w (западный). Таким образом, state(w, w, w, w) означает, что все объекты расположены на западном берегу. Эта ситуация соответствует началу решения задачи. Следует отметить, что все описанные соглашения были произвольно выбраны автором. На самом деле, как отмечают специалисты в области искусственного интеллекта, выбор соответствующего представления - зачастую наиболее критичный аспект решения задачи. Выбранные соглашения обеспечивают удобное представление логики предикатов на языке PROLOG. Различные состояния мира создаются в результате переправы через реку и представляются изменениями значений параметров предиката state (рис. 14.1). Возможны также и другие представления. Теперь опишем общий граф для данной проблемы переправы. Пока не будем учитывать тот факт, что некоторые состояния являются неустойчивыми. На рис. 14.2 показано начало графа возможных переправ через реку. Поскольку человек всегда управляет лодкой, то нет необходимости вводить отдельное представление для местоположения лодки. На рис. 14.2 показана часть графа, на котором необходимо выполнять поиск пути. Описанный выше рекурсивный вызов предиката path обеспечивает механизм управления поиском в продукционной системе. Продукционные правила - это правила изменения состояния в процессе поиска. Определим их на языке PROLOG как правила move. Поскольку язык PROLOG оперирует хорновскими дизъюнктами, то продукционные правила на языке PROLOG должны либо напрямую описываться в хорновской дизъюнк- 626 Часть VI. Языки и технологии программирования для искусственного интеллекта тивной форме, либо преобразовываться к этому формату. Выберем первый вариант (процесс приведения правил "если..., то..." к хорновской дизъюнктивной форме описан в разделе 12.2). В хорновской дизъюнктивной форме шаблоны текущего и последующего состояний должны располагаться в голове дизъюнктивного выражения, т.е. слева от оператора : -. Они являются аргументами предиката move. Условия, которым должно удовлетворять продукционное правило для возвращения следующего состояния, размещаются справа от оператора : -. Как будет показано в следующем примере, эти условия также могут быть представлены в виде ограничений унификации. Рис. 14.1. Пример последовательности перевозок для задачи переправы человека, волка, козы и капусты Сначала определим правила для перевозки через реку волка. Это правило должно учитывать перевозку как с левого берега на правый, так и наоборот, а также должно быть применимо, если человек и волк находятся на противоположных берегах реки. Таким образом, оно должно преобразовывать состояние state (e, e, G, С) в состояние state (w, w, G, С), а состояние state (w, w, G, C)-в state (e, e, G, С). Для состояний state (с, w, G, С) и state (w, e, G, С) оно должно принимать значение "ложь". Переменные G и С представляют тот факт, что третий и четвертый параметры могут быть связаны с любым из значений е или w. Независимо от значений этих параметров они не изменяются после переправы человека и волка. Некоторые из полученных состояний могут оказаться "неустойчивыми". Следующее правило move применимо только в том случае, если человек и волк находятся на одном и том же берегу и переправляются на противоположный берег реки. Заметим, что местоположение козы и капусты не изменяется. move(state(X,X,G,C), state(Y,Y,G,C)) :- opp(X, Y). opp(e, w). opp(w, e). Глава 14. Введение в PROLOG 627 Рис. 14.2. Часть графа состояний для задачи переправы человека, волка, козы и капусты с учетом неустойчивых состояний Это правило срабатывает в том случае, если в качестве первого параметра предикату move передается состояние (текущее положение на графе), соответствующее одинаковому местоположению человека и волка. При активизации этого правила генерируется новое состояние (второй параметр предиката move), в котором значение Y противоположно значению X. Для получения этого нового состояния должны быть выполнены два условия. Во-первых, должны совпадать значения первых двух параметров, а во-вторых, оба новых местоположения должны быть противоположны старым. Первое условие проверяется неявно в процессе унификации. Следовательно, если первые два параметра не совпадают, то предикат move даже не вызывается. Эту проверку можно явно выполнить с помощью следующего правила. move(state(F, W, G, С), state(Z, Z, G, C)) :- F=W, opp(F, Z). В этом эквивалентном правиле move сначала проверяется равенство значений F и W, и только при его выполнении (расположении обоих объектов на одном берегу реки) параметру Z присваивается значение, противоположное F. Заметим, что в языке PROLOG присваивание можно реализовать с помощью связывания значений переменных в процессе унификации. Связанные значения распространяются на все вхождения переменной в дизъюнктивном выражении, и область действия переменной ограничивается содержащим ее дизъюнктом. В процессе усечения поиска важную роль играет такое мощное средство программирования систем искусственного интеллекта, как проверка соответствия шаблонам. Состояния, не удовлетворяющие шаблонам, автоматически отсекаются для данного правила. В этом смысле первая версия правила move обеспечивает более эффективное представление, поскольку, если первые два параметра не совпадают, то в процессе унификации это состояние даже на рассматривается. Теперь создадим предикат, позволяющий проверить безопасность каждого нового состояния, чтобы в процессе переправы через реку ни один из объектов не был съеден. Не- 628 Часть VI. Языки и технологии программирования для искусственного интеллекта безопасным является любое состояние, в котором второй и третий параметры совпадают и противоположны первому параметру. В этом случае волк съест козу. Кроме того, небезопасным является и такое состояние, при котором одинаковы третий и четвертый параметры, и при этом они противоположны первому. Тогда коза съест капусту. Эти небезопасные ситуации можно представить в виде следующих правил unsafe. unsafe(state(X, Y, Y, С)) :- opp(X, Y). unsafe(state(X, W, Y, Y)) :- opp(X, Y). Здесь необходимо отметить несколько моментов. Во-первых, если состояние "не является небезопасным" not unsafe (т.е. безопасно), то согласно определению встроенной процедуры not в языке PROLOG для этого состояния не может быть истинным ни один из предикатов unsafe. Следовательно, ни один из этих предикатов не может быть унифицирован с текущим состоянием, или при унификации их условия не должны выполняться. Во-вторых, встроенная процедура not не эквивалентна операции логического отрицания -1 в теории предикатов первого порядка. Процедура not - это, скорее, отрицание за счет невыполнения противоположного утверждения. Чтобы проверить, как поведет себя предикат unsafe, необходимо протестировать множество состояний. Добавим к предыдущему продукционному правилу проверку not unsafe. move(state(X, X, G, С), state(Y, Y, G, C)) :- opp(X, Y), not(unsafe(state(Y, Y, G, C))). Процедура not unsafe проверяет приемлемость нового состояния в процессе поиска с помощью вызова предиката unsafe. Если все критерии удовлетворяются, включая проверку (в алгоритме path) отсутствия этого состояния в списке уже пройденных состояний, для данного состояния вызывается (рекурсивно) алгоритм path для перехода глубже по графу. При вызове предиката path новое состояние добавляется к списку уже пройденных. Аналогично можно разработать три других продукционных правила, представляющих переправу через реку человека с козой, капустой и без "пассажиров". Для вывода результатов трассировки добавим к каждому правилу команду writelist. Команда reverse_print_stack используется в условии останова алгоритма path для вывода пути окончательного решения. И, наконец, добавим пятое "псевдоправило", которое не содержит никаких условий и поэтому всегда срабатывает при невыполнении всех предыдущих правил. Оно означает возврат вызова алгоритма path из текущего состояния. Это псевдоправило позволяет пользователю отслеживать поведение продукционной системы в процессе ее работы. Теперь рассмотрим полную программу на языке PROLOG, реализующую продукционную систему решения задачи переправы человека, волка, козы и капусты. Она должна включать предикаты unsafe и writelist, а также предикаты, описывающие абстрактный тип данных стек (подраздел 14.2.1). move (state (X, X, G, С) , state (Y, Y, G, С) ) :- opp(X, Y) not(unsafe(state(Y, Y, G, C))), writelist(['попытка переправить человека и волка', Y, Y, G, С] ) . move(state(X, W, X, С) , state (Y, W, Y, С) ) :- орр(Х, Y), not(unsafe(state(Y, W, Y, С) ) ) , writelist(['попытка переправить человека и козу', Y, W, Y, С] ) . move (state (X, W, G, X) , state (Y, W, G, Y) ) :- opp(X, Y), not(unsafe(state(Y, W, G, Y) )), writelist(["попытка переправить человека и капусту', Y, W, G, Y] ) . Глава 14. Введение в PROLOG 629 move (state (X, W, G, С) , state(Y, W, G, С) ) :- opp(X, Y), not(unsafe(state(Y, W, G, C))), writelist(['попытка переправить одного человека', Y, W, G, С] ) . move (state (F, W, G, С) , state (F, W, G, С) ) :- writelist([v ВОЗВРАТ из', F, W, G, C]), fail, path(Goal, Goal, Been_stack):- write("Путь решения: '), nl, reverse_print_stack(Been_stack). path(State, Goal, Been_stack):- move(State, Next_state), not(member_stack(Next_state, Been_stack)), stack(Next_state, Been_stack, New_been_stack) , path(Next_state, Goal, New_been_stack),!. opp(e, w). opp (w, e) . Этот код вызывается в запросе go, инициализирующем рекурсивный вызов алгоритма path. Чтобы упростить работу программы, можно создать предикат test, облегчающий ввод начальных данных. go(Start, Goal) :- empty_stack(Empty_been_stack), stack(Start, Empty_been_stack, Been_stack), path(Start, Goal, Been_stack). test :- go(state(w, w, w, w), state(e, e, e, e)). Этот алгоритм возвращается из состояний, не обеспечивающих дальнейшего продвижения. Для мониторинга связывания различных переменных в процессе каждого вызова алгоритма path можно использовать команду trace. Следует отметить, что эта система является общей программой перемещения четырех объектов из любого (допустимого) местоположения на берегах реки в любое другое (допустимое) местоположение, включая поиск обратного пути к начальному состоянию. Другие интересные свойства этой продукционной системы, в том числе зависимость результатов поиска на графе от порядка следования правил, можно проиллюстрировать в следующих упражнениях. Приведем некоторые результаты трассировки выполнения программы, отображая лишь правила, действительно используемые для генерации новых состояний. ?- test. попытка переправить человека и козу е w e w попытка переправить одного человека w w e w попытка переправить человека и волка е е е w попытка переправить человека и козу w e w w попытка переправить человека и капусту е е w e попытка переправить человека и волка w w w e попытка переправить человека и козу е w e e ВОЗВРАТ из е, w, e, e ВОЗВРАТ из w, w, w, e попытка переправить одного человека w e w e попытка переправить человека и козу е е е е Путь решения: state (w, w, w, w) state(e, w, e, w) state (w, w, e, w) 630 Часть VI. Языки и технологии программирования для искусственного интеллекта state(e, e, e, w) state (w, e, w, w) state(e, e, w, e) state(w, e, w, e) state(e, e, e, e) Итак, эта программа на языке PROLOG реализует продукционную систему решения задачи переправы человека, волка, козы и капусты. Правила move составляют продукционную память. Рабочая память представлена аргументами вызова алгоритма path. Механизм управления продукционной системой реализуется с помощью рекурсивного вызова path. И, наконец, порядок следования правил для генерации дочерних состояний (разрешения конфликта) определяется порядком расположения этих правил в продукционной памяти. 14.4. Разработка альтернативных стратегий поиска Как видно из предыдущего раздела, в самом языке PROLOG реализован поиск в глубину с возвратом. Этот момент более детально будет описан в разделе 14.7. Теперь рассмотрим, как реализовать на языке PROLOG альтернативные стратегии поиска, описанные в главах 3-5. Для записи состояний в процессе поиска в глубину, в ширину и при реализации алгоритма поиска будут использованы списки open и closed. В случае неудачного завершения поиска в некоторой точке мы не будем возвращаться к предыдущим значениям этих списков. Они будут обновляться при вызове алгоритма path, и поиск будет продолжаться с новыми значениями. Для предотвращения хранения старых версий списков open и closed будет использован оператор отсечения. 14.4.1. Поиск в глубину с использованием списка closed Поскольку при возвратах в рекурсивных вызовах значения переменных восстанавливаются, алгоритм поиска в глубину из раздела 14.3 пополняет список пройденных состояний только состояниями, относящимися к текущему пути к цели. И хотя проверка наличия в списке каждого нового состояния позволяет избежать циклов, она требует повторного тестирования тех областей пространства, которые уже были достигнуты ранее при построении других путей, но отклонены на данный момент из-за их бесперспективности. Это более полное множество состояний составляет список, получивший название closed в главе 3 и Closed_set в следующем алгоритме. В множестве Closed_set хранятся все состояния текущего пути и состояния, отвергнутые при возврате алгоритма. Следовательно, теперь оно не представляет путь от исходного к текущему состоянию. Для хранения информации о пути определим упорядоченную пару [State, Parent], представляющую каждое состояние и его родителя. Начальное состояние Start будет представлено парой [Start, nil]. Эти пары будут использованы для воссоздания пути решения из множества Closed_set. Опишем на языке PROLOG структуру программы для поиска в глубину, отслеживая значения списков open и closed и проверяя каждое новое состояние на его наличие в списке пройденных. Алгоритм path зависит от трех параметров: стека Open_stack, Closed_set (обрабатываемого как множество) и целевого состояния Goal. Текущее состояние State является следующим состоянием для Open_stack. Операторы stack и set описаны в разделе 14.2. Глава 14. Введение в PROLOG 631 Поиск начинается с предиката до, инициализирующего вызов алгоритма path. Заметим, что при выполнении команды до в стек Open_stack помещается пара, описывающая начальное состояние с нулевым родителем [Start, nil]. Множество Closed_set пока пусто. go(Start, Goal) :- , empty_stack(Empty_open), stack([Start, nil], Empty_open, Open_stack), empty_set(Closed_set), path(Open_stack, Closed_set, Goal). Вызов алгоритма path, зависящего от трех аргументов, имеет вид. path(Open_stack, _, _) :- empty_stack(Open_stack), write("3TM правила не привели к решению'). path(Open_stack, Closed_set, Goal) :- stack([State, Parent], _, Open_stack), State = Goal, write('Решение найдено!'), nl, printsolution([State, Parent], Closed_set). path(Open_stack, Closed_set, Goal) :- stack([State, Parent], Rest_open_stack, Open_stack), get_children(State, Rest_open_stack, Closed_set, Children), add_list_to_stack(Children, Rest_open_stack, New_open_stack), union([[State, Parent]], Closed_set, New_closed_set), path(New_open_stack, New_closed_set, Goal), !. get_children(State, Rest_open_stack, Closed_set, Children) :- bagof(Child, moves(State, Rest_open_stack, Closed_set, Child), Children). moves(State, Rest_open_stack, Closed_set, [Next, State]) :- move(State, Next), not(unsafe(Next)), % проверка зависит от задачи not(member_stack([Next, _], Rest_open_stack)), not(member_set([Next, _], Closed_set)). Предполагается, что правила move и (при необходимости) предикат unsafe некоторым образом определены. move(Present_state, Next_state) :- ... % первое правило move(Present_state, Next_state) :- ... % второе правило При вызове первой команды path поиск прекращается, если стек Open_stack пуст. Это означает, что в списке open больше не содержатся состояния для продолжения поиска. Обычно это свидетельствует о том, что для данного графа выполнен поиск полным перебором. Второй вызов path завершается при нахождении решения, которое и выводится на печать. Поскольку состояния графа поиска представлены парами [State, Parent], команда printsolution перейдет ко множеству Closed_set и рекурсивно перестроит путь решения. Заметим, что путь решения будет выведен в прямом порядке. printsolution([State, nil], _) :- write(State), nl. printsolution([State, Parent], Closed_set) :- member_set ( [Parent, Grandparent], Closed_set), printsolution([Parent, Grandparent], Closed_set), write(State), nl. 632 Часть VI. Языки и технологии программирования для искусственного интеллекта В третьем вызове path используется стандартный для многих интерпретаторов языка PROLOG предикат bagof. Он позволяет хранить все подстановки унификации для данного шаблона в едином списке. Второй параметр предиката bagof - это шаблон, с которым производится сравнение в базе данных. Первый параметр определяет подлежащие хранению в списке компоненты второго параметра. Например, нас могут интересовать связанные значения для одной переменной предиката. Все связанные переменные первого параметра накапливаются в списке и связываются с третьим параметром. В этой программе предикат bagof накапливает состояния, достигнутые при срабатывании всех существующих продукционных правил. Естественно, необходимо хранить все потомки конкретного состояния, поэтому их можно добавить в соответствующем порядке в список open. Вторым аргументом предиката bagof является новый предикат moves, вызывающий предикаты move для генерации всех состояний, которые могут быть достигнуты с помощью данного продукционного правила. Аргументами предиката moves являются текущее состояние, списки open и closed и переменная, представляющая состояние, достигаемое при "хорошем" ходе. Прежде чем возвратить это состояние, предикат moves проверяет, что это новое состояние Next не содержится в множествах rest_open_stack, open (поскольку текущее состояние удалено) и closed_set. Предикат bagof вызывает moves и накапливает все состояния, удовлетворяющие этим условиям. Третий аргумент предиката bagof представляет новые состояния, помещенные в стек Open_stack. В некоторых реализациях предикат bagof принимает значение "ложь", если для второго аргумента не найдено никаких соответствий, а значит, третий аргумент пуст. Это можно исправить с помощью подстановки (bagof (X, moves (S, Т, С, X), List) ; List= [ ] ) для текущих вызовов предиката bagof в данном коде. Поскольку состояния поиска представлены парами "состояние-родитель", предикаты проверки вхождения элемента во множество member__set необходимо модифицировать таким образом, чтобы они отражали структуру проверки соответствия шаблону. Необходимо проверить, совпадает ли пара "состояние-родитель" с первым элементом списка таких пар, и, если нет - выполнить рекурсивное сравнение со всеми остальными элементами списка. member_set([State, Parent], [[State, Parent]| _]). member_set(X, [_|T]) :- member_set(X, T). 14.4.2. Поиск в ширину в языке PROLOG Теперь рассмотрим оболочку (shell) алгоритма поиска в ширину, явно использующую списки open и closed. Эту оболочку совместно с правилами move и предикатами unsafe можно применять для решения любой задачи поиска. Алгоритм вызывается следующим образом. go(Start, Goal) :- empty_gueue(Empty_open_gueue), enqueue([Start, nil], Empty_open_queue, Open_gueue), empty_set(Closed_set) , path(Open_queue, Closed_set, Goal). Значения параметров Start и Goal очевидны. Они представляют начальное и целевое состояния. В этом алгоритме, как и при поиске в глубину, снова создаются упорядоченные пары [State, Parent] для хранения информации о каждом состоянии и его родителе. Начальное состояние Start представляется парой [Start, nil]. Эта ин- Глава 14. Введение в PROLOG 633 формация используется командой printsolution для воссоздания пути решения на основе множества Closed_set. Первым параметром предиката path является открытая очередь Open_queue, вторым - множество Closed_set, а третьим- целевое состояние Goal. Переменные, значения которых не учитываются в дизъюнктивном выражении, обозначаются символом _. path(Open_queue, _, _) :- empty_queue(Open_queue), write('Поиск завершен, решение не найдено'). path(Open_queue, Closed_set, Goal) dequeue([State, Parent], Open_queue, _), State = Goal, write("Путь решения:'), nl, printsolution([State, Parent], Closed_set). path(Open_queue, Closed_set, Goal) :- dequeue([State, Parent], Open_queue, Rest_open_queue), get_children(State, Rest_open_queue, Closed_set, Children), add_list_to_queue(Children, Rest_open_queue, New_open_queue) , union([[State, Parent]], Closed_set, New_closed_set), path(New_open_queue, New_closed_set, Goal), !. get_children(State, Rest_open_queue, Closed_set, Children) :- bagof(Child, moves(State, Rest_open_queue, Closed_set, Child), Children). moves(State, Rest_open_queue, Closed_set, [Next, State]) :- move(State, Next), not(unsafe(Next)), ^проверка зависит от задачи not(member_queue([Next, _], Rest_open_queue)), not(member_set([Next, _], Closed_set)). Этот фрагмент кода назван оболочкой, поскольку здесь не описаны правила move. Их необходимо добавить с учетом конкретной предметной области задачи. Операторы queue и set описаны в разделе 14.2. Первое условие останова алгоритма path определяется для случая, когда предикат path вызывается с пустым первым аргументом Open_queue. Эта ситуация возникает, только если для графа больше не осталось непроверенных состояний, а решение не было найдено. Решение находится при вызове второго предиката path, когда голова очереди Open_queue совпадает с целевым состоянием Goal. В процессе поиска пути предикаты bagof и moves накапливают все дочерние состояния для текущих состояний и поддерживают очередь. Работа этих предикатов описана в предыдущем разделе. Чтобы воссоздать путь решения, каждое состояние сохраняется в виде пары [State, Parent]. Родителем начального состояния является указатель nil. Как упоминалось в подразделе 14.4.1, попарное представление состояний требует некоторого усложнения проверки соответствия шаблонам в предикатах member, moves и printsolution. 14.4.3. Реализация "жадного" алгоритма поиска на языке PROLOG Оболочка для реализации "жадного" алгоритма поиска - это модификация программы поиска в ширину, в которой открытая очередь для каждого нового вызова path заменяется приоритетной очередью, упорядоченной по эвристическому критерию. В предлагаемом алгоритме эвристическая мера неразрывно связана с каждым новым состоянием открытой очереди и применяется для систематизации ее элементов. Мы будем также 634 Часть VI. Языки и технологии программирования для искусственного интеллекта хранить информацию о родителе каждого состояния. Как и при реализации поиска в ширину, она используется для построения пути решения командой printsolution. Чтобы отслеживать всю необходимую для поиска информацию, каждое состояние представим в виде списка из пяти элементов: описание состояния, родительское состояние, целое число, определяющее глубину поиска на графе, целое число, определяющее эвристическую меру качества состояния, и целочисленная сумма третьего и четвертого элемента. Первый и второй элементы списка находятся обычным образом, третий определяется путем добавления единицы к предыдущей глубине, четвертый - это эвристическая мера для конкретной задачи. Пятый элемент списка используется для упорядочения состояний в открытой очереди Open_pq и вычисляется по формуле f(n)=g(n)+h(n) (см. главу 4). Как и ранее, правила move не приводятся. Они определяются с учетом специфики задачи. Операторы работы с очередью и приоритетной очередью описаны в разделе 14.2. Мера качества состояния heuristic тоже определяется с учетом специфики задачи и отражает эвристический характер четвертого параметра в списке описаний. Этот алгоритм имеет два условия останова и вызывается следующим образом. gо(Start, Goal) :- empty_set(Closed_set), empty_pq(Open), heuristic(Start, Goal, H), insertjpq([Start, nil, 0, H, H], Open, Open_pq), path(Open_pq, Closed_set, Goal). Здесь nil - это родительское состояние для состояния Start, a H - его эвристическая мера качества. Приведем код программы, реализующей "жадный" алгоритм поиска. path(Open_pq, _, _) :- empty_pq(Open_pq), write("Поиск завершен, решение не найдено.'). path(Open_pq, Closed_set, Goal) :- dequeue__pq( [State, Parent, _, _, _] , Open_pq, _) , State = Goal, write("Путь решения:'), nl, printsolution([State, Parent, _, _, _], Closed_set). path(Open_pq, Closed_set, Goal) :- dequeue_pq([State, Parent, D, H, S], Open_pq, Rest_open_pq), get_children([State, Parent, D, H, S], Rest_open_pq, Closed_set, Children, Goal), insert_list_pq(Children, Rest_open_pq, New_open_pq), union([[State, Parent, D, H, S]], Closed_set, New_closed_set), path(New_open_pq, New_closed_set, Goal), !. Предикат get_children генерирует все дочерние состояния для состояния State. Как и в предыдущих алгоритмах поиска, в нем используются предикаты bagof и moves. Работа этих предикатов более подробно описана в подразделе 14.4.1. Правила переходов, проверки безопасности допустимых переходов и эвристическая мера качества определяются с учетом конкретной задачи. Проверка принадлежности элемента множеству должна быть специально реализована для списков из пяти элементов. get_children([State,_, D, _, _], Rest_open_pq, Closed_set, Children, Goal) :- bagof(Child, moves([State, _, D, _, _], Rest_open_pq, Closed_set, Child, Goal), Children). Глава 14. Введение в PROLOG 635
Смотрите здесь бизнес практикум Синергии, to. |
|
|
© FILOSOF.HISTORIC.RU 2001–2023 Все права на тексты книг принадлежат их авторам! При копировании страниц проекта обязательно ставить ссылку: 'Электронная библиотека по философии - http://filosof.historic.ru' |