Язык программирования SWI-Prolog. Учебно-методическое пособие
.pdfго вычисления fib(K) можно будет взять уже вычисленное значение. Для этого достаточно добавить в начало программы строку
:-table(fib/2).
Число 2 после слеша – это арность предиката, т. е. количество его параметров. После этого все представленные выше значения вычисляются практически мгновенно (не более 0.1 секунды).
Другой способ – оставить в алгоритме только один рекурсивный вызов в конце предиката, чтобы получить так называемую хвостовую рекурсию.
Для этого мы будем передавать пару соседних чисел Фибоначчи через список параметров предиката.
fib(N,R):- fib(N,0,1,R).
fib(1,_,R,R):- !. fib(N,F1,F2, R):-
N>1,
N1 is N-1,
NextF is F1 + F2, fib(N1, F2, NextF, R).
В этом случае вычисление чисел Фибоначчи также происходит практически мгновенно.
Дополнение. В SWI-Prolog реализованы операции длинной арифметики, т. е. мы можем получить правильные значения чисел, превышающие ограничение 2*10^9, характерное для большинства«обычных» языков программирования!
?- fib(90, R).
R = 2880067194370816120
Из приведенных примеров ясно, что Пролог лучше всего приспособлен для реализации рекурсивных алгоритмов.
Пример. Алгоритм быстрой сортировки состоит из двух шагов:
1)разбиение исходного списка на два: меньших некоторого значения и больших или равных этому значению. Часто в качестве такого значения выбирают первый элемент списка;
2)рекурсивная сортировка каждого из полученных списков.
Реализуем предложенный алгоритм.
Для разбиения списка напишем вспомогательный предикатlist_parts с четырьмя аргументами:
21
list_parts(барьер, исходный_список, список_меньших, список_больших).
list_parts(_,[],[],[]). – пустой список делится на два пустых;
list_parts(X, [H|T], [H|U], V):- H < X, list_parts(X, T, U, V). – если значе-
ние H (головы исходного списка) меньше значения барьерного элемента, то H добавляется в первый список, причем это добавление «спрятано» в аргумент предиката!
list_parts(X, [H|T], U, [H|V]):- H>=X, list_parts(X, T, U, V). – если значе-
ние H (головы исходного списка) больше или равно значению барьерного элемента, то H добавляется во второй список, причем это добавление также «спрятано» в аргумент предиката!
Основной предикат
quicksort([],[]). quicksort([X|T],R):-
list_parts(X,T,U,V), quicksort(U,SU), quicksort(V,SV), append(SU,[X|SV],R).
1.4. Отсечение
В предыдущих примерах нам несколько раз встретился оператор отсечения, задаваемый восклицательным знаком. В Прологе он играет очень важную роль, позволяя сократить перебор вариантов при выполнении запроса и/или отбросить заведомо неверные варианты.
Пример. Уберём отсечение в предикате abs:
abs(X,Y):-
X>=0, Y is X; Y is -X.
и выполним запрос abs(7,Y). Получим:
Y = 7
Y = -7
Почему так? Поскольку точка с запятой есть оператор ИЛИ, то Пролог выполняет предикаты, образующие первый операнд X>=0 И Y is X, запоминает точку возврата, «сообщает» пользователю результат Y = 7 и при нажатии кнопки Next продолжает выполнение предиката abs с этой точки.
22
Отсечение фактически убирает точку возврата, поэтому второй операнд выполняться не будет.
Рассмотрим ещё один вариант предикатаabs. Добавим во второй операнд проверку условия X < 0:
abs(X,Y):-
X>=0, Y is X; X<0, Y is -X.
тогда последовательно получим:
Y = 7 и
false ( при нажатии кнопки Next).
В этом случае точка возврата всё равно создаётся, только оператор Y is -X не выполняется, так как условие X < 0 ложно.
1.5. Структуры данных: стек, очередь, дерево
Эти структуры данных строятся на основе списков. Напомним сначала определения.
Стек – это линейный список, в котором все включения и исключения делаются в голове списка.
Очередь – это линейный список, в котором все включения делаются в хвосте списка, а исключения – в голове списка.
Дерево – структура данных, состоящая из корневой вершины и конечного числа подчиненных деревьев.
Наиболее просто Пролог работает со стеком, так как список в Прологе естественным образом интерпретируется как стек.
Пример. Дан стек – список натуральных чисел. Из стека извлекается два числа. Если оба числа одинаковой чётности, то в стек добавляется половина их суммы. Если оба числа разной чётности, то в стек добавляется половина их произведения. В конце концов, в стеке останется одно значение. Выведите его на экран.
Разберём тестовый пример.
Исходные данные – список S = [ 7, 3, 10, 14, 5, 12, 6 ] мы рассматриваем как стек, при этом вершина стека есть число 7.
Шаг 1. Извлекаем числа 7 и 3. Так как они оба одной чётности, заменя-
ем их значением (7+3) div 2 = 5. Получим: S = [ 5, 10, 14, 5, 12, 6 ].
23
Шаг 2. Извлекаем числа 5 и 10. Так как они разной чётности, то заме-
няем их значением (5*10) div 2 = 25. Получим: S = [ 25, 14, 5, 12, 6 ].
На каждом шаге стек-список уменьшается на одно число. В конце концов в стеке останется значение 153.
Напишем программу.
solve([R],R). |
% |
стек состоит из одного элемента |
solve(S,R):- |
% |
в стеке есть несколько элементов |
S=[X,Y|T], |
% |
извлекаем первые два числа из стека |
% если |
|
|
X mod 2 =:= Y mod 2, % сравниваем чётности чисел |
||
% то |
|
2, |
Z is (X+Y) div |
||
%добавляем в стек половину суммы
S1=[Z|T],
solve(S1,R),!;
%иначе
S=[X,Y|T], |
% |
извлекаем те же числа из стека |
|
Z |
is (X*Y) |
div |
2, |
% |
добавляем в стек половину произведения |
||
S1=[Z|T],
solve(S1,R),!.
Проверим работу программы на тестовом примере:
S = [7,3,10,14,5,12,6], solve(S,R).
Результат:
R = 153,
S = [7, 3, 10, 14, 5, 12, 6]
Теперь приведём решение аналогичной задачи для очереди. Дана очередь натуральных чисел. Из очереди одновременно извлекается два числа. Если оба числа одинаковой чётности, то в конец очереди добавляется половина их суммы. Если оба числа разной чётности, то в конец очереди добавляется половина их произведения. В конце концов, в очереди останется одно значение. Выведите его на экран.
Очередь от стека отличается операцией добавления элемента– новое значение добавляется в конец очереди. Для реализации этой операции используем предикат append.
solve([R],R). % очередь состоит из одного элемента solve(S,R):- % в очереди есть несколько элементов
24
S=[X,Y|T], % |
извлекаем первые два числа из очереди |
|
% если |
|
2, |
X mod 2 =:= Y mod |
||
% то |
2, |
|
Z is (X+Y) div |
|
|
%добавляем в очередь половину суммы append(T,[Z],S1),
solve(S1,R),!;
%иначе
S=[X,Y|T], |
% |
извлекаем те же числа из очереди |
|
Z |
is (X*Y) |
div |
2, |
% |
добавляем в очередь половину произведения |
||
append(T,[Z],S1), solve(S1,R),!.
Проверим работу программы на тестовом примере:
S = [7,3,10,14,5,12,6], solve(S,R).
Результат:
R = 18,
S = [7, 3, 10, 14, 5, 12, 6]
Дерево можно реализовать с помощью вложенных списков. Для простоты будем рассматривать только бинарные деревья, каждой вершине которых принадлежит не более двух поддеревьев.
Списку D = [7, [3, [10, [], []], [14, [], []]], [5, [12, [], []], [6, [], []]]] соот-
ветствует бинарное дерево
Для большей наглядности пустые ветви в описании дерева можно -за менить на «пустой» предикат nil:
D = [7, [3, [10, nil, nil], [14, nil, nil]], [5, [12, nil, nil], [6, nil, nil]]]
Задача 1. Вычислить сумму элементов дерева.
Решение. Дерево является рекурсивной структурой. Представим его в виде D = [Root, Left, Right], где Root – корень дерева, Left – левое поддере-
25
во, Right – правое поддерево. Тогда для нахождения суммы всех элементов необходимо найти сумму элементов левого поддереваSL, сумму элементов правого поддерева SR, тогда общая сумма будет равна Root + SL + SR. Приведём программу на Прологе, реализующую описанный алгоритм.
summa([],0). % 0 при отсутствии ветви summa(D, S):-
D=[Root,Left,Right],
summa(Left,SL), summa(Right,SR), S is Root+SL+SR,!.
Выполним запрос:
D = [7,[3,[10,[],[]],[14,[],[]]],[5,[12,[],[]],[6,[],[]]]], summa(D, S).
Результат:
D = [7, [3, [10], [14]], [5, [12], [6]]],
S = 57
Проверим также правильность работы программы в случае отсутствия левой или правой ветви.
D = [7,[3,[],[14]],[5,[12],[]]], summa(D, S).
Результат:
D = [7, [3, [], [14]], [5, [12], []]],
S = 41
Задача 2. Напишите предикат, который получает список неповторяющихся целых чисел и строит бинарное упорядоченное дерево, т. е. дерево, в котором все элементы левого поддерева меньше значения в корне, а все элементы правого поддерева больше значения в корне, причем это же правило применимо и для любого поддерева.
Решение. Напишем сначала вспомогательный предикат для вставки нового значения в упорядоченное дерево. Рассмотрим сначала случай вставки значения X в пустое дерево:
insert(X,[],[X,[],[]]).
В результате X становится корнем дерева.
Пусть теперь дерево не пусто. Тогда сравниваем X с корнем дерева Root. Если X < Root, то X нужно вставить в левое поддерево, если же X>=Root, то X нужно будет вставить в правое поддерево.
26
insert(X,[Root,Left,Right],[Root,NewLeft,Right]):- X<Root, insert(X,Left,NewLeft).
insert(X,[Root,Left,Right],[Root,Left,NewRight]):- X>=Root, insert(X,Right,NewRight).
Теперь можно написать основной предикат:
create(L,Tree,NewTree):- L=[X|T], insert(X,Tree,TmpTree), create(T,TmpTree,NewTree),!.
Когда список L будет исчерпан, переменная NewTree должна будет получить значение переменной Tree:
create([],Tree,NewTree):-NewTree=Tree.
Это правило можно записать короче: create([],Tree,Tree). Тестовый пример. Запрос:
L=[7,12,4,6,1], create(L,[],R).
Результат:
L = [7, 12, 4, 6, 1],
R = [7, [4, [1, [], []], [6, [], []]], [12, [], []]]
1.6. Предикаты высших порядков
Общее свойство таких предикатов– они получают другой предикат в качестве параметра. Эти предикаты реализуют парадигму функционального программирования в Прологе.
findall(объект, цель, список) – формирует список из объектов, которые удовлетворяют цели. Цель представляет собой набор предикатов и условий.
Пример. Дан список натуральных чисел. Отобрать в новый список значения, которые делятся на 13 и не превосходят 100.
Программа:
main(L, Res):-
findall(X, (member(X, L), X=<100, X mod 13 =:= 0), Res).
Запрос:
L = [130, 91, 26, 50, 250], main(L,Res).
27
Результаты:
L = [130, 91, 26, 50, 250],
Res = [91, 26]
Заметим, что в этом примере оператор сравнения=:= нельзя заменить оператором =, но можно использовать операторis: 0 is X mod 13. Аналогично, если мы заменим в условии задачи требование делимости на13 противоположным, то необходимо использовать оператор =\=, а не оператор \=. Альтернативный вариант: not(0 is X mod 13).
maplist(предикат, исходный_список, результат) приме-
няет указанный предикат ко всем элементам исходного списка. Пример. Напишем предикат возведения в степень
pow(K,X,R):-R is X^K.
и возведем в квадрат значения из списка запросом
maplist(pow(2), [1,2,3,4], R), !.
Результат:
R = [1,4,9,16]
Приведём программный код предиката
maplist(_, [], []). maplist(Pred, [A|As], [B|Bs]) :-
call(Pred, A, B),
maplist(Pred, As, Bs).
Здесь call вызывает предикат Pred, передаёт ему аргумент A, а результат вызова записывает в B. Предикат call позволяет выполнять различные предикаты в процессе работы программы. В частности, программист может передавать в качестве параметра произвольную функцию (предикат).
Пример. Используя метод трапеций, вычислить приближенно значение определенного интеграла от f(x) на отрезке [a;b].
Решение.
f(X,Y):-Y is X*X. g(X,Y):-Y is sin(X).
tr(_,_,_,N,N,0):-!. tr(F,A,B,N,K,R):-
D is (B-A)/N,
28
X0 is A + D*K, X1 is X0 + D, call(F,X0,Y0), call(F,X1,Y1), K1 is K+1, tr(F,A,B,N,K1,R1), R is R1 + D*(Y0+Y1)/2.
main(F,A,B,N,Res):- tr(F,A,B,N,0,Res).
Запрос: main(f,0,1,100,Res). Результат: Res = 0.33335
Запрос: main(g,0,pi,100,Res).
Результат: Res = 1.999835503887444
1.7. Решение задач
Для закрепления теоретического материала приведём решенияне скольких типичных задач.
Задача 1. Вычислить сумму S = 1 + 1/4 + 1/9 + 1/16 + … + 1/n2. Этот ряд при увеличении числа слагаемых стремится к значению R = pi2/6.
Решение. Более простой вариант решения получается при суммировании ряда от конца к началу. Искомую сумму можно представить в виде рекуррентного соотношения S(n) = S(n-1) + 1/n2. Следовательно, сначала нужно вычислить S(n-1), а затем прибавить к полученной сумме последнее слагаемое. Но S(n-1) вычисляется через S(n-2), которое вычисляется через S(n-3) и т. д., пока мы не дойдём доS(1). Чтобы сохранить общую схему, примем, что S(1) = S(0) + 1, а S(0) = 0. Тем самым мы получаем правило остановки рекурсии:
sum(0,0):- !.
Напишем теперь основной предикат в соответствии с изложенным выше алгоритмом:
sum(K, S):- K1 is K-1, sum(K1,S1),
S is S1 + 1/(K*K).
Тестовый вариант запроса: sum(2, S).
29
Последовательность вызовов: K = 2, sum(2,S) => K = 1, sum(1, S) => K = 0, S = 0.
Последовательность возвратов: K = 1, S is 0 + 1/(1*1) = 1, K = 2, S is 1 + 1/(2*2) = 1.25
Приведём также вариант суммирования ряда от начала к концу. Такой вариант можно рекомендовать, если каждое следующее слагаемое намного проще вычислить через предыдущее, чем вычислять его «с нуля».
Задача 2. Вычислить сумму S = 1 + 1/1! + 1/2! + 1/3! + … + 1/n!. Этот ряд при увеличении числа слагаемых стремится к значению R = e (основанию натуральных логарифмов).
Решение. Обозначим K – номер очередного слагаемого при нумерации элементов ряда с нуля, N – номер последнего слагаемого, V – значение текущего слагаемого, W – текущая сумма, S – результат.
Напишем предикат
sum(K, N, V, W, S):- |
% правило остановки |
|
||
K>N, S = W, !; |
|
|
||
% программируем шаг |
рекурсии |
|
|
|
K1 is K+1, |
|
|
|
|
V1 is V/K1, |
|
|
|
|
W1 is W + V1, |
|
|
|
|
sum(K1, N, V1, W1, S). |
|
|
||
При вызове предиката передадим начальные значения параметров: |
|
|||
стартовое значение номера элемента рядаK = 0; |
|
|
||
номер последнего слагаемогоN = 20; |
|
|
||
значение начального элемента рядаV = 1; |
|
|
||
значение начальной суммы рядаW = 1. |
|
|
||
Выполним |
запрос: |
sum(0,20,1,1,S). |
Получим |
число |
S = 2.7182818284590455, которое, кроме последней |
цифры, совпадает |
со |
||
значением числа e. |
|
|
|
|
Задача 3. Написать предикат, который получает два целых числа K и N (K < N) и заполняет массив числами K, K+1, …, N-1. Если же K>=N, то результат – пустой список.
30
