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

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

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

Или короче:

randList(N,K,[H|T]):-

H is random(K), N1 is N-1, randList(N1, T).

1.2.7. Встроенные предикаты для работы со списками

Эти предикаты находятся в библиотекеlists, которая загружается автоматически. Если этого нет, то библиотеку можно подключить программно:

:- use_module(library(lists)).

Основные предикаты:

length(List, Length) – вычисление/задание длины списка; min_list(List, MinElem) – вычисление наименьшего элемента списка; max_list(List, MaxElem) – вычисление наибольшего элемента списка; sum_list(List, Summa) – вычисление суммы элементов списка;

sort(List, SortedList) – сортирует список по возрастанию, при этом дубликаты значений удаляются. Поскольку список может содержать разнородные элементы, то при сортировке используются следующие соотношения:

переменные < числа < строки < атомы < термы.

Переменные сортируются по адресу, числа – по значению, строки и атомы сортируются по алфавиту, термы сначала сортируются по их арности, затем по имени функтора и, наконец, по их параметрам.

Заметим, что при равенстве вещественного и целого значений вещественное число считается меньшим!

Более гибким является вариант sort(Key, Order, List, SortedList), кото-

рый позволяет управлять сортировкой сложных объектов и параметрами вывода.

Параметр Key в случае составных объектов определяет номер компонента, по которому будет выполнена сортировка. При сортировке атомарных значений Key = 0. Параметр Order может принимать следующие значения:

@< задаёт сортировку по возрастанию с удалением дубликатов; @=< задаёт сортировку по возрастанию с сохранением дубликатов; @> задаёт сортировку по убыванию с удалением дубликатов; @=> задаёт сортировку по убыванию с сохранением дубликатов.

11

Таким образом, sort(List, SortedList) эквивалентно sort(0, @<, List, SortedList)

msort(List, SortedList) – сортирует список по возрастанию, но при этом дубликаты значений НЕ удаляются. msort(List, SortedList) также эк-

вивалентно sort(0, @=<, List, SortedList);

keysort(Pairs, SortedList) – сортирует список пар по значению ключа,

эквивалентно sort(1, @=<, Pairs, SortedList);

delete(List, V, Res) – из списка List удаляются все вхождения значения V; reverse(List, ReverseList) – предикат переворота списка; member(Elem, List) – выполняет поиск значения в списке. Предикат

имеет значение true, если элемент Elem присутствует в списке. Предикат состоит из двух правил:

1)если значение первого элемента списка совпадает со значением искомого элемента – правило завершается удачей;

2)в противном случае первый элемент отбрасывается, и поиск продолжается в хвосте. Как только разделение списка на голову и хвост станет невозможно – правило завершится неудачей.

Приведём текст предиката:

member(Elem, [Elem|_]). member(Elem, [_|Tail]):- member(Elem, Tail).

Заметим, что мы фактически реализовали здесь оператор ветвления. Сначала Пролог проверяет первое правило– условие совпадения значения Elem с головой списка. При совпадении возвращается true, иначе Пролог переходит к выполнению второго правила. Заметим, что в программах на Прологе важен порядок фактов и правил, так как они проверяются сверху вниз!

append(List1, List2, Result) – объединение двух списков в один в указанном порядке. Пример.

Запрос: L1 = [3, 1, 12], L2 = [7], append(L1,L2,Res). Результат: Res = [3, 1, 12, 7].

Этот же предикат можно использовать и для других целей, например, чтобы получить разность списков Res и L1 или Res и L2:

Запрос: L1 = [3, 1, 12], Res = [3, 1, 12, 7], append(L1,L2,Res).

Результат: L2 = [7].

Запрос: L2 = [12, 7], Res = [3, 1, 12, 7], append(L1,L2,Res). Результат: L1 = [3, 1].

12

Рассмотрим вспомогательные предикаты nth0(K, List, Elem, Rest) и nth1(K, List, Elem, Rest). Они возвращают true, если на K-том месте в списке List стоит значение Elem. Предикаты различаются только тем, что nth0 нумерует элементы списка, начиная с 0, а nth1 – начиная с 1. Эти предикаты можно также использовать для вычисления позиции элемента в списке, для вставки элемента в список, для извлечения/удаления элемента из списка. Необязательный параметр Rest есть «остаток» списка после удаления K-того элемента.

Примеры.

1) Проверка места элемента в списке:

Запрос: nth1(2, [1,12,3,5], 12)

Результат: true

Запрос: nth1(2, [1,13,3,5], 12)

Результат: false

2) Вычисление позиции элемента в списке:

Запрос: List = [1,12,3,5], nth1(K, List, 12)

Результат – значение 12 находится на втором месте:

K = 2,

List = [1, 12, 3, 5]

3) Вставка элемента в список:

Запрос: Rest = [1,3,5], nth1(2, List, 12, Rest)

Результат – список List, полученный из Rest добавлением значения 12:

List = [1, 3, 5, 12],

Rest = [1, 3, 5]

4) Извлечение/удаление элемента:

Запрос: List = [1,12,3,5], nth1(2, List, Elem, Rest)

Результат – извлекаемый элемент записывается в переменнуюElem, список после удаления – в переменную Rest:

Elem = 12,

List = [1, 12, 3, 5],

Rest = [1, 3, 5]

1.2.8. Структура программы

Программа на языкеSWI-Prolog представляет собой совокупность утверждений (фактов), предикатов (правил) и цели (запроса). Выполнение

13

программы сводится к поиску значений свободных переменных, при которых цель станет истинной.

Пример. Пусть нам известно, что Иван любит хоккей, Петр любит футбол, и справедливо утверждение: Иван любит что-то, если Петр любит то же самое. Утверждения «Иван любит хоккей» и «Петр любит футбол» запишутся на Прологе в виде фактов

любит('Иван', хоккей). любит('Петр', футбол).

Правило «Иван любит что-то, если Петр любит то же самое» в Прологе запишется так:

любит('Иван', X):- любит('Петр', X).

Запись каждого факта и правила заканчивается точкой.

На основе имеющихся правил и фактов Prolog делает выводы. Допустим, мы хотим узнать, что любит Иван. Составим запрос (цель

программы):

?- любит('Иван', X).

Пользуясь встроенными механизмами, Пролог последовательно сопоставляет цель и факты и сначала обнаружит, что «Иван любит хоккей». Затем анализирует правило и выводит из него, что «Иван любит футбол, так как Петр любит футбол». Обратите внимание, что этого факта в программе нет, т. е. Пролог самостоятельно обнаружил новый факт! В результате Пролог выведет

X = хоккей

X = футбол

А если задать запрос: «Кто любит футбол?», то Пролог найдет все решения этой задачи.

?- любит(X, футбол). X = 'Петр'

X = 'Иван'

1.3. Ветвление, рекурсия, циклы

Заметим, что ни ветвлений, ни циклов в стандартном понимании этого термина в Прологе нет. Ветвление в Прологе реализуется через варианты предиката или логическое ИЛИ(;), а циклы естественно заменяются рекурсивными предикатами.

14

1.3.1. Ветвление

Пример. Напишем предикат abs(X,Y) для вычисления абсолютной величины числа.

Первый вариант: формулируем два правила.

abs(X,X):- X>=0,!. abs(X,Y):- Y is -X.

Первое правило: если X > 0 то выходной параметр получает значение X и выполнение предиката на этом заканчивается, поскольку в конце правила стоит оператор отсечения (!). Если же первое правило ложно, то реализуется второе (оно же последнее) правило, и выполнение предиката заканчивается автоматически.

Второй вариант: используем логическую операцию ИЛИ, задаваемую точкой с запятой:

abs(X,Y):-

X>=0, Y is X,!; Y is -X.

Третий вариант: используем операцию «стрелка» (->):

abs(X,Y):-

X>0 -> Y is X; Y is -X.

В любом варианте следует учитывать главную особенность ветвления: если условие выполнения ветви ложно, то не будет выполнен ни один оператор этой ветви!

Пример:

test(A,R):-

D is A^2 - 4, D<0, R is 2*D,!; R is sqrt(D).

Запрос test(1,R) вычисляет D = -3, и так как D < 0, возвращает значение R = -6. Однако при запросе test(6,R) получаем сообщение об ошибке:

Arguments are not sufficiently instantiated In:

[2] _1394>=0

[1] test(6,_1428) at line 1

Становится ясно, что ошибка возникла из-за того, что переменная D не была конкретизирована, т. е. оператор D is A^2 - 4 не был выполнен!

15

1.3.2. Рекурсия

Рекурсивный предикат – это предикат, который вызывает сам себя. Чтобы не попасть в бесконечную последовательность рекурсивных вызовов, необходимо в предикат включить правило остановки.

Пример. Написать предикат, который получает список S и возвращает список R из первых K элементов исходного списка.

Решение.

copy(S,K,R):- % если

K=0, R=[],!;

%иначе выполняем вторую часть предиката

S=[H|T], K1 is K-1,

%выполняем рекурсивный вызов copy(T,K1,R1),

%возвращаемся в предикат

R=[H|R1].

Выполним запрос:

copy([1,2,3,4,5],2,R).

При первом вызове предиката K = 2, поэтому будет выполняться вторая

 

часть, содержащая вызов copy([2,3,4,5], 1, R). При этом первый элемент

 

списка (значение 1) будет сохранен в локальной переменой H.

 

 

При втором вызове K = 1, и снова выполняется вторая часть предиката.

 

Значение первого элемента списка теперь равно2, оно сохраняется в но-

 

вой локальной переменной H и вызывается copy([3,4,5],0,R).

 

 

 

При третьем вызове срабатывает условие K = 0, создаётся переменная

 

R, которая получает значение [] – пустой список. После этого мы возвра-

 

щаемся

в

локальную

среду

предыдущего

вызова

и

полу

R = [2|[]] = [2], так как значение локальной переменной H было равно 2. Так как тело предиката закончилось, мы выходим из него и попадаем в

локальную среду предыдущего вызова, здесь H = 1, следовательно, значение локальной переменной R становится равным [1|[2]], т. е. R = [1,2]. Теперь мы выходим из первого вызова и завершаем работу программы.

Таким образом, часть вычислений мы делаем при переходе по цепочке вызовов, а часть – при цепочке возвратов из рекурсивного предиката. Если рекурсивный вызов стоит последним в предикате, то при возврате из предиката ничего делать не нужно. Это так называемая хвостовая рекурсия и Пролог успешно заменяет её итерацией.

16

1.3.3. Циклы

Реализацию цикла через рекурсию проиллюстрируем на простом примере.

for(0):-!.

%

условие останова/выхода из цикла

for(K):-

%

тело цикла

writeln(K),

K1 is K-1, for(K1).%

переход к следующей итерации

Здесь в теле цикла выводится на экран значение его параметра. Поскольку рекурсивный вызов стоит последним в предикатеfor, это так называемая хвостовая рекурсия и Пролог оптимизирует код, превращая рекурсию в итерацию, поэтому в этом случае время работы предиката минимально.

Аналогично можно моделировать цикл с условием:

while(K):- read(K), K=0,!; while(K).

В этом примере цикл ввода повторяется до тех пор, пока не будет введен 0. Заметим, что пример носит искусственный характер, так как оператор read позволяет ввести список значений целиком.

Пример. Выполняя запрос read(L), наберём в строке ввода [4,-12,3.14], получим результат: L = [4, -12, 3.14].

Однако моделирование циклов всё же является искусственным приёмом. Более естественно воспользоваться возможностями, заложенными в язык его разработчиками.

Пример. Имеются гири в 1, 3, 9 и 16 кг. Как с их помощью отвесить N кг нефасованного сахарного песка на двухчашечных весах, если гири можно класть на разные чашки весов?

Пусть нам нужно отвесить 2 кг сахара. Тогда на одну чашку весов положим гирю в 3 кг, а на другую – гирю в 1 кг и 2 кг сахара.

Заметим, что для некоторых N задача может иметь несколько решений, например, при N = 13 получаем два решения:

16 кг (гиря) = 3 кг (гиря) + 13 кг (сахар) и 1 кг (гиря) + 3 кг (гиря) + 9 кг (гиря) = 13 кг (сахар).

17

Подобные задачи решаются перебором вариантов. Введём переменные Q1, Q3, Q9, Q16 – «весовые» коэффициенты, которые равны -1, если соответствующая гиря лежит на одной чашке с взвешиваемым сахаром, 1, если гиря лежит на другой чашке весов, и 0, если эта гиря не используется для взвешивания.

В этом случае вес сахара будет вычисляться по формуле

N is Q1*1 + Q3*3 + Q9*9 + Q16*16.

Полный перебор гарантирует, что будут найдены все решения задачи. В процедурном языке такой перебор проще всего реализовать с помощью вложенных циклов:

для Q1 = -1, 0, 1 выполнить для Q3 = -1, 0, 1 выполнить

для Q9 = -1, 0, 1 выполнить для Q16 = -1, 0, 1 выполнить

если Q1*1 + Q3*3 + Q9*9 + Q16*16 равно N то вывести Q1, Q3, Q9, Q16

кц

кц

кц

кц

Существенным преимуществом Пролога является бэктрекинг– автоматическая реализация алгоритма перебора с возвратами, так как это освобождает программиста от необходимости явной организации такого перебора.

Приведём программу на Прологе. Для визуализации хода выполнения программы добавим в программу отладочную печать:

qf(-1). qf(0). qf(1). main(Q):-

read(N),

Q = [Q1,Q3,Q9,Q16],

qf(Q1), qf(Q3), qf(Q9), qf(Q16), writeln(Q), % отладочная печать

N is Q1*1 + Q3*3 + Q9*9 + Q16*16.

18

Проследим ход выполнения программы при N = 13. При первом проходе при вызове каждого из предикатов qf(Q1), qf(Q3), qf(Q9), qf(Q16) переменным Q1, Q3, Q9, Q16 ставится в соответствие значение -1 и создаются точки возврата. Так как в последней строке программы значение N = 13 не совпадает со значением правой части оператораis, Пролог возвращается к последней точке возврата и выполняет предикат qf(Q16), в результате чего переменная Q16 получит значение 0, а остальные переменные не изменятся. Снова нет совпадения, снова возврат и Q16 получает значение 1.

Начало отладочной печати подтверждает теорию:

[-1, -1, -1, -1] [-1, -1, -1, 0] [-1, -1, -1, 1]

Так как все значения предиката qf(Q16) исчерпаны, следующий возврат произойдёт к предикатуqf(Q9), связанному с переменнойQ9, после исчерпания значений этой переменной – к предикату, связанному с переменной Q3, и т. д. То есть мы фактически организовали перебор вариантов, не используя в явном виде циклы!

После получения первого решения Q = [0, -1, 0, 1] Пролог (после нажатия кнопки Next) продолжает работу программы, так как в ней остались точки возврата, пока не будет закончен перебор всех вариантов. Приведём завершающую часть отладочной печати (полужирным шрифтом выделено попавшее среди отладочной печати одно из решений задачи для N = 13):

[1, 1, 1, -1] [1, 1, 1, 0]

Q = [1, 1, 1, 0]

[1, 1, 1, 1] false

Всё, перебор закончен!

Замечание. Предикат qf можно записать в альтернативном виде:

qf(X):-

X is -1; X is 0; X is 1.

19

В ряде случаев избавиться от рекурсии невозможно. Рассмотрим, например, вычисление N-ного значения в последовательности чисел Фибоначчи. В процедурных языках программирования можно написать цикл вида:

F0=0; F1=1;

для K от 2 до N выполнить

F = F0 + F1 // K-тое число Фибоначчи

F0 = F1

F1 = F

кц вывод F

Сложность такого алгоритма равна О(N). Рекурсивный алгоритм может быть таким:

функция F(N): // предполагаем, что N > 0

начало

если N<2 то вернуть 1

иначе вернуть F(N-1) + F(N-2)

конец

Сложность такого алгоритма равна O(2^N), так как количество вычислений удваивается при увеличении глубины рекурсии на 1.

Запишем рекурсивный алгоритм на Прологе и выполним его для -не скольких значений N.

fib(0,0):-!. fib(1,1):-!. fib(N,R):-

N1 is N-1, fib(N1,R1),

N2 is N-2, fib(N2,R2), R is R1 + R2.

?- fib(30, R).

R =

832040,

0.739 seconds cpu time

?- fib(35, R).

R =

9227465,

8.142 seconds cpu time

?- fib(40, R).

R = 102334155,

90.492 seconds cpu time

(Важны не сами цифры, которые зависят от параметров конкретного компьютера, а стремительный (экспоненциальный) рост времени работы программы.)

Решить эту проблему можно стандартным способом– хранить вычисленные на каком-то шаге значения fib(K) в памяти, тогда вместо повторно-

20

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