Основы логического программирования в среде Турбо Пролог. Учебное пособие по курсу «Экспертные системы»
.pdfот тех же параметров, которые указаны в заголовке правила. Параметры должны изменяться на каждом шаге так, чтобы в итоге либо сработал базис рекурсии, либо условие выхода из рекурсии, размещенное в самом правиле.
Внекоторых ситуациях предложений, реализующих базис рекурсии,
ипредложений, описывающих шаг рекурсии, может быть несколько. Как правило, это бывает в сложных случаях, например, когда выполняемые в процессе реализации шага рекурсии действия зависят от выполнения или невыполнения какого-либо условия.
Приме р 1. Создадим предикат, который будет вычислять по натуральному числу его факториал. Напомним, что
1!=1 (факториал единицы равен единице)
N!=(N −1)! N (для того чтобы вычислить факториал некоторого числа, нужно вычислить факториал числа на единицу меньшего и умножить его на исходное число).
Для нашего примера программа на Турбо Прологе запишется в
виде: p redicates
factorial(integer, real)
clauses
factorial (1,1).
factorial (N ,F):- N 1=N -1, factorial (N 1,F1), F=F1*N . goal
factorial (3, X ), write(X ).
Ответом будет число «6».
Вданном примере базисом рекурсии является первое правило, шагом рекурсии – второе правило. Во втором правиле последовательность «N 1=N -1, factorial (N 1,F1)» служит для вычисления факториала числа на единицу меньшего исходного числа (F1). Причем «factorial (N 1,F1)» есть рекурсивный вызов. Цель «F=F1*N » вычисляет факториал исходного числа N .
Приме р 2. Рассмотрим один из способов организации простого циклического повторения с помощью рекурсии, связанной с наличием в базе знаний процедуры вида:
rep eat. rep eat:- rep eat.
Использование rep eat в качестве подцели некоторого правила приводит к многократному повторению остальных подцелей этого правила. При наличии его в качестве целевого утверждения в правиле:
31
1.Предикат rep eat согласуется с базой данных, так как имеется
соответствующий факт – первое утверждение определения предиката rep eat.
2.Припопыткепоискаальтернативногорешениябудетвыполняться второе правило, которое за счет рекурсивного вызова самого себя успешно согласуется (вновь срабатывает первое правило) и так далее. Целевое утверждение rep eat может быть согласовано бесконечное число раз.
Покажем действие предиката rep eat в программе:
p redicates rep eat clauses
rep eat. rep eat:-rep eat.
goal
rep eat, random(6,X ), Y=X +1, write(“\n”,Y), fail.
Приведенная программа осуществляет циклический вывод на экран случайного числа в диапазоне от 1 до 6 включительно. Предикат random(6,X ) согласуется, если X – случайное целое число в диапазоне от 0 до 5. Выражение Y=X +1 сдвигает этот диапазон вверх на единицу. Полученное случайное число Y выводится на печать предикатом write(“\n”,Y). Последовательность «\n» указывает на необходимость вывода с новой строки. Для того, чтобы Пролог постоянно повторял описанную последовательность, требуется неуспех цели (обеспечивается предикатом fail) – в этом случае система начнет поиск альтернативного решения с предиката rep eat, которое, разумеется, будет найдено.
Доработав цель,
rep eat, random(6,X ), Y=X +1, write(“\n”,Y), readchar(Z), Z=’. ‘.,
получим более интерактивный вариант программы: теперь после вывода каждого случайного числа происходит считывание символа с кла-
виатуры (readchar(Z)), что приводит к паузе до нажатия любой клавиши.
Кроме того, если нажата клавиша «. » – цель будет согласована и Пролог завершит работу программы, нажатие любой другой клавиши приводит к несогласованию цели и началу поиска альтернативного варианта решения.
Следует отметить, что рекурсивные программы часто дают компактные описания интересующих нас отношений, однако недостаток их состоит в том, что во время счета по таким программам может образовываться увеличивающийся в размерах стек латентных вызовов, для
32
хранения которого может потребоваться неприемлемый объем памяти. Как правило, рекурсией пользуются тогда, когда либо эти затраты памяти компенсируются элегантностью программы, либо не имеется практических нерекурсивных алгоритмов для решения поставленной задачи.
2.2. Пре дставление списков
Список – упорядоченная последовательность элементов, которая может иметь произвольную длину. Объектами списка могут быть константы, переменные, структуры. Порядок элементов в последовательности является существенным. Элементы списка заключаются в квадратные скобки [], друг от друга отделяются запятыми.
Список, не содержащий элементов, называется пустым (или нулевым).
Приведем пример списка:
[ элеме нт 1, элеме нт 2, элеме нт 3. . . ]
Списки могут содержать другие списки и переменные. Например, в Прологе допустимы следующие списки:
[кошка, машина, [ловить, рыба, мясо]] [a, III, [X ,Y]]
Работа со списками основана на расщеплении их на голову и хвост списка. Головой списка является первый его элемент. Хвост списка представляет собой список, состоящий из всех элементов исходного списка, за исключением первого его элемента. Этот новый список можно делить и дальше.
Так, для списка [ элеме нт 1, элеме нт 2, элеме нт 3. . . ] элеме нт 1 –
это голова, а хвостом является список
[элеме нт 2, элеме нт 3. . . ].
ВПрологе введена специальная форма для представления списка с головой Х и хвостомY. Это записывается как [X |Y], где для разделения Х
иY используется вертикальная черта.
Следующие примеры демонстрируют разделение списков на голову и хвост:
Список |
Голова |
Хвост |
[a, b, c] |
a |
[b, c] |
[[this, car], go] |
[this, car] |
[go] |
[go,[this, car]] |
go |
[this, car] |
[] |
- |
- |
33
Пустой список не имеет ни головы, ни хвоста.
Графически список можно представить в виде дерева (рис. 4) либо в виде диаграммы «виноградная лоза» (рис. 5). Конец списка обычно представляют как хвост, который является пустым списком.
На рисунках 4, 5 в качестве примера приведено графическое пред-
ставление списка [a, III, [X ,Y]].
Проанализируем диаграмму «виноградная лоза» (рис. 5). Из рисунка видно, что каждый ее горизонтальный уровень является списком, состоящим из некоторого числа элементов. Верхний уровень представляет список, состоящий из трех элементов, один из которых сам является списком. Второй уровень, содержащий два элемента, представляет третий элемент списка верхнего уровня.
A 

III
[X , ] []
AIII
Турбо Пролог позволяет выполнять над списками следующие операции: проверку на принадлежность к списку, добавление, удаление, перестановку элементов списка, сортировку элементов списка, объединение списков.
Р ис . 4. Графическое представление списка в виде дерева
[]
[]
X
Р ис . 5. Графическое представление списка в виде диаграммы «виноградная лоза»
34
2.3. Опе рации над списками
Для использования в программе списка необходимо произвести описание домена и предиката списка.
ВТурбо Прологе, в связи со строгой типизацией, все элементысписка должны принадлежать одному домену.
Описание домена списка осуществляется в разделе domains с указанием звездочки * после имени типа элемента списка.
domains list=char*
Вразделе описания предикатов p redicates требуется присутствие предиката списка. Если одним из его аргументов является список, то за именем этого предиката в круглых скобках указывается имя домена списка на месте соответствующего аргумента.
p redicates name(…, list, …)
Принадлеж ность элеме нта списку
Создадим предикат member, позволяющий проверить принадлежность элемента списку. Аргументами предиката являются искомый элемент и список, в котором производится поиск:
member(9, [5, 7, 9, 11, 13]).
Для определения истинности предиката необходима проверка двух условий:
1.искомый элемент X совпадает с головой списка Y: member(X , [X| _]). или member(X , [Y| ]): – X =Y.
2.элемент Х является элементом хвоста этого списка: member(X , [_|Y]): – member (X , Y).
Приме р. Пусть имеется некоторый список [5, 7, 9, 11, 13]. Необходимо определить, содержится ли в заданном списке число «9».
Для нашего примера программа на Турбо Прологе запишется в виде:
domains list=integer* element=integer
p redicates member(element, list).
35
clauses
member(X , [X| _]).
member(X , [_|Y]): – member(X , Y).
goal
member(9, [5, 7, 9, 11, 13]).
Ответом будет «Yes».
Удале ние дублирующих элеме нтов
Для удаления дублирующих элементов списка можно определить следующий предикат:
delete(S1, S2),
где S1 – исходный список, S2 – выходной список без дублирующих элементов.
Если исходный список пуст, то выходной тоже пуст. Если же исходный список не пустой, то он представляется в виде головы и хвоста списка и проверяется, принадлежит ли голова списка хвосту. Если это условие выполняется, то это значит, что в хвосте есть дубликат головы, и тогда выходной список формируется их хвоста списка. Невыполнение условия означает, что голова дубликатов не имеет. Только в этом случае (так как использована операция отсечения «!») происходит включение головы списка в выходной список и формируется хвост выходного списка из хвоста исходного списка. Для проверки принадлежности элемента хвосту списка используется описанный выше предикат member.
Приме р. Удалить из списка дублирующие элементы. domains
number=integer
listsymb=symbol*
list=symbol* p redicates
delete(list, list). member(symbol, listsymbol).
clauses delete([], []).
delete([X| S1], S2):– member(X , S1), !, delete(S1, S2). delete([X| S1], [X| S2]):– delete(S1, S2).
member(X , [X| _]).
member(X , [X| _]):– member(X , T). goal
delete([1, 2, 3, 3,7], L).
36
Добавле ние и удале ние элеме нта из списка
Добавление элемента в список
Наиболее простой способ добавить элемент в список - это вставить его в самое начало так, чтобы он стал его новой головой. Если Х - это новый элемент, а список, в который Х добавляется - L, тогда результирующий список будет иметь вид:
[X| L]
Таким образом, для того, чтобы добавить новый элемент в начало списка, не надо использовать никакой процедуры. Тем не менее, если мы хотим определить такую процедуру в явном виде, то ее можно представить в форме такого факта:
add(X , L, [X| L]).
Приме р. В список L = [3, 7, 8] добавить элемент «2». domains
list=integer*
element=integer
p redicates
add(element, list, list).
clause
add(X , L, [X| L]).
goal
add(2, [3, 7, 8], L).
Результатом работы программы будет список L = [2, 3, 7, 8].
Удаление элемента из списка
Удаление элемента Х из списка L можно запрограммировать, используя следующий предикат delete(X , L, L1), где L1 совпадает со списком L, у которого удален элемент X . При этом необходимо рассмотреть два случая:
1) Если X является головой списка, тогда результатом удаления будет хвост этого списка:
delete(X , [X| L], L).
2) Если X находится в хвосте списка, тогда его нужно удалить оттуда: delete(X , [Y|L], [Y|L1):– delete(X , L, L1).
37
Приме р. Удалить элемент «2» из списка L= [1, 2, 3, 12]. domains
list=integer*
element=integer p redicates
delete(element, list, list). clauses
delete(X , [X| L], L).
delete(X , [Y|L], [Y|L1]):– delete(X , L, L1). goal
delete(2, [1, 2, 3, 12], L1).
Результатом работы программы будет L1 = [1, 3, 12].
Обраще ние списка
Пустой список после обращения – пустой. Обратить список [X| L1] и получить список L2 можно, если обратить список L1 в L3 и в хвост ему добавить X .
domains list=integer*
p redicates
app end(list, list, list). reverse(list, list).
clauses
app end([], L, L).
app end([X| L1], L2, [X| L3]): – app end(L1, L2, L3). reverse([ ],[ ]).
reverse([X| L1], L2): – reverse(L1, L3), app end( L3, [X ], L2). goal
reverse ([1, 2, 3], L2).
Результатом работы программы будет L2 =[3, 2, 1].
Опре де ле ние длины списка
Создадим предикат length(L,N ), позволяющий вычислить длину списка L, т. е. количество элементов в списке N .
Для пустого списка, в котором нет элементов, имеет место запись: length([], 0).
Количество элементов непустого списка N , представленного в виде
объединения первого элемента и хвоста Y, равно количеству элементов
хвоста (M), увеличенному на единицу:
38
length([_|Y], N ):– length(Y, M), N = M + 1.
При переходе от всего списка к его хвосту нам не важно, чему равен первый элемент списка, поэтому мы используем анонимную переменную.
Приме р. Определить количество элементов в списке [1, 2, 3]. domains
list=integer*
int=integer p redicates
length (list, int) clauses
length([], 0).
length([_|Y], N ):– length(Y, M), N = M + 1. goal
length([1,2,3], N ).
Результатом работы программы будет N = 3.
Объе дине ние и разде ле ние списков
Объединение списков
Определим предикат append слияния двух списков для создания нового списка, использующий рекурсию и выполняющий процедуру сцепления (конкатенации). Если присоединить пустой список [] к списку L, то получим список L:
app end([], L, L).
Пусть необходимо присоединить список [X |L1] к списку L2, результатом будет список [Х|L3], где L3 – список, получаемый при соединении
L1 с L2:
app end([X| L1], L2, [X| L3]): – app end(L1, L2, L3).
Первый элемент первого списка (Х) всегда будет первым элементом третьего списка. Хвост списка L3 всегда будет представлять результат присоединения второго списка L2 к хвосту первого списка L1.
Приме р. Объединить списки L1[1,2,6] и L2[6,8,12]. domains
list=integer* p redicates
app end(list, list, list).
39
clauses
app end([], L, L).
app end([X| L1], L2, [X| L3]): – app end(L1, L2, L3). goal
app end([1,2,6], [6,8,12], L).
Результатом работы программы будет список L = [1,2,6,6,8,12].
Разделение списка
В случае деления списка Lна список L1 (например, для которого элементы меньше значения М) и L2 (например, значение элементов больше или равно М) вводится следующий предикат:
del(M, L, L1, L2).
Приме р. Разделить список L =[1, 2, 7, 20, 21] согласно следующему
условию:
L1,еслиX < 5 |
||
X |
|
|
L2 |
,еслиX >= |
5. |
domains list=integer*
p redicates
del(integer, list, list, list). clauses
del(M, [], [], L2):– H < M, del(M, T, L1, L2).
del(M, [H|T], L1, [H|L2]):– H >= M, del(M, T, L1, L2).
del( , [], [], []). goal
del(5, [1, 2, 7, 20, 21], L1, L2).
Результатом работы программы будут списки L1 = [1,2],
L2 = [7,20,21].
Сортировка списков
Сортировка вставкой
Сортировка представляет собой упорядочивание элементов списка определенным образом.
40
