Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Язык программирования SWI-Prolog. Учебно-методическое пособие

.pdf
Скачиваний:
0
Добавлен:
15.08.2026
Размер:
830 Кб
Скачать

го вычисления 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

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]