Язык программирования SWI-Prolog. Учебно-методическое пособие
.pdfРешение. Из условия задачи сразу следует правило остановки рекурсии:
list_asc(K, N, []):-K>=N,!.
Результатом является пустой список. Основной предикат формирует выходной список, вставляя в него значение K как голову списка, а для вычисления хвоста вызывается тот же предикат с параметром K1, равным K+1:
list_asc(K, N, [K|T]):-
K1 is K+1,list_asc(K1, N, T).
Задача 4. Написать предикат, который вычисляет число положительных элементов в списке.
Решение. Число элементов пустого списка равно нулю:
nump([],0).
Если список не пуст, то разделяем его на голову и хвост и анализируем знак головы списка. В случае положительного значения увеличиваем значение счетчика на единицу и снова вызываем основной предикат, иначе просто вызываем основной предикат:
nump(L, K):-
L=[A|T],
( A>0, nump(T,W), K is W+1,!;
nump(T,K) ).
Наличие круглых скобок, отделяющих оператор ветвления от оператора разделения списка L = [A|T], существенно для корректной работы предиката. Если скобки убрать, то L = [A|T] попадает в ветвь «то» оператора ветвления. Если условие A > 0 не выполнится, то и разделения списка на голову и хвост не произойдёт и переменнаяT, которая нужна для выполнения ветви «иначе», не будет конкретизирована, и мы получим сообщение об ошибке.
Более простой вариант получится, если в предикат передавать список, уже представленный в виде[голова|хвост], т. е. разобранный на части. В этом случае дополнительные скобки нам не понадобятся.
nump([],0).
nump([A|T], K):-
A>0, nump(T,W), K is W+1,!;
nump(T,K).
31
Вариант с использованием встроенных предикатов:
nump(L,K):-
findall(X, (member(X,L),X>0), R), length(R,K).
Задача 5. Написать предикат, вычисляющий значение многочлена P(X)
в заданной точке. Многочлен P(X) = A0 +A1*X +A2*X^2 + … + AN*X^N
задаётся списком своих коэффициентов A0, A1, …, AN.
Решение. Значение многочлена легко вычисляется методом Горнера без возведения в степень. Идея метода:
Представим P(X) в виде P(X) = A0 + X*P1(X).
Здесь P1(X) = A1 + X*P2(X), P2(X) = A2 + X*P3(X), P3(X) = A3 + X*P4(X) и т. д.
В этой схеме ясно видна рекурсия! Напишем основной предикат:
poly_val(L,X,Res):-
L=[H|T], poly_val(T,X,R), Res is H + X*R.
Правило остановки: когда список коэффициентов будет исчерпан, предикат должен вернуть нулевое значение:
poly_val([],_,0).
1.8. Программирование с ограничениями
SWI-Prolog содержит три библиотеки, которые позволяют задавать ограничения на область значений переменных, а также ограничения в виде равенств и неравенств. Это библиотеки:
clpfd (Constraint Logic Programming over Finite Domains) – для задач с це-
лочисленными переменными. Библиотека часто используется для решения логических и комбинаторных задач, в частности, в документации приведён пример программы, которая получает решение логической игры Судоку;
clpr (Constraint Logic Programming over Rationals and Reals) – для задач с вещественными переменными. Библиотека используется для решения задач линейного и нелинейного программирования. Заметим, что в SWIProlog также есть библиотека simplex, предназначенная только для решения задач линейного программирования, но там используется другой синтаксис описания задачи;
32
clpb (Constraint Logic Programming over Boolean Variables) – для задач с логическими переменными.
В настоящем пособии мы ограничимся библиотекой clpfd. Рассмотрим основные возможности библиотеки. Ограничения сравнения. Эти ограничения имеют вид:
A операция B,
где A и B – некоторые выражения. Обозначим значения этих выражений через valueA и valueB соответственно. Тогда
A #= B – ограничение valueA равно valueB;
A #\= B – ограничение valueA не равно valueB;
A #>= B – ограничение valueA больше или равно valueB; A #=< B – ограничение valueA равно или меньше valueB; A #> B – ограничение valueA строго больше valueB;
A #< B – ограничение valueA строго меньше valueB.
Сюда же можно отнести предикатchain(список, ограничение), который задаёт одно и то же ограничение на все пары соседних элементов списка.
Пример.
L = [A, B, C], chain(L, #<) эквивалентно ограничениям A #< B, B #< C.
Принадлежность множеству (диапазону) значений: in диапазон – для простой переменной;
ins диапазон – для всех элементов списка.
Диапазон может состоять из одного числа, последовательности целых чисел или являться объединением диапазонов. Возможно задание неограниченных диапазонов, для этого используются ключевые словаinf, если диапазон не ограничен снизу, и sup, если диапазон не ограничен сверху.
Примеры.
X in 0..2 – переменная X может принимать только значения 0, 1 или 2; X in 1..3 \/ 5 – переменная X может принимать значения 1, 2, 3 или 5; [A, B, C] ins 1\/7\/13 – каждая из переменных A, B, C может принимать
значения 1, 7 или 13.
Если на разные элементы списка накладываются разные ограничения, то можно воспользоваться предикатом maplist:
maplist(in, список_переменных, список_диапазонов).
33
Например,
maplist(in, [A,B,C], [-1..2, 0..7, 6..9]),
что эквивалентно A in -1..2, B in 0..7, C in 6..9.
Для конкретизации значений переменных используется предикатlabel. Приведём полный пример.
:- use_module(library(clpfd)). main(L):-
length(L,3),
maplist(in, L, [-1..2, 0..7, 6..9]), label(L).
Пояснения:
1)предикат length(список, длина) определяет количество элементов существующего списка или создаёт список из анонимных переменных, если задана длина, но сам список ещё не существует. Если же заданы оба параметра предиката, то предикат только проверяет корректность данных;
2)предикат label последовательно конкретизирует элементы спискаL по возрастанию значений диапазонов. То есть в нашем примере будет получена последовательность результатов:
L = [-1, 0, 6], L = [-1, 0, 7],
...
L = [2, 7, 9].
Без label мы бы получили «решение» в следующем виде:
L = [_2082, _2088, _2094], _2082 in -1..2,
_2088 in 0..7, _2094 in 6..9
Полученный результат фактически есть подстановка внутренних имён анонимных переменных в условие задачи (символ подчеркивания с последующим числовым значением является внутренним именем анонимной переменной). Заметим, кстати, что все анонимные переменные– это разные переменные!
При необходимости изменить порядок получения результатов нужно использовать предикат labeling, который предоставляет программисту возможность настройки порядка выдачи.
34
Предикат label можно использовать для генерации комбинаторных объектов.
Пример. Получить все последовательности из цифр 0 и 1 длиной 3.
:- use_module(library(clpfd)). main(L):-
length(L,3), L ins 0..1, label(L).
В ряде задач требуется получить перестановки элементов некоторого множества. В этом случае наряду с предикатомlabel нужно использовать предикат all_distinct(список), который проверяет, что все элементы списка отличны друг от друга.
Пример. Получить все перестановки чисел 1, 2 и 3.
:- use_module(library(clpfd)). main(L):-
length(L,3), L ins 1..3,
all_distinct(L), label(L).
Запрос: main(L).
Результат:
L = [1, 2, 3]
L = [1, 3, 2]
L = [2, 1, 3]
L = [2, 3, 1]
L = [3, 1, 2]
L = [3, 2, 1]
Этот же подход можно использовать и для генерации размещений. Необходимо только, чтобы количество мест для размещения было меньше количества элементов исходного множества. Так, если в предыдущем примере указать length(L, 2), то мы получим
L = [1, 2]
L = [1, 3]
L = [2, 1]
L = [2, 3]
L = [3, 1]
L = [3, 2]
35
Для генерации сочетаний нужно помнить, что сочетания от размещений отличаются тем, что порядок следования значений в сочетании не важен. То есть если зафиксировать определённый порядок значений в размещениях, то программа генерации размещений превратится в программу генерации сочетаний. Это легко сделать с помощью предикатаchain. Приведём пример генерации сочетаний по три значения из четырёх возможных:
:- use_module(library(clpfd)). main(L):-
length(L,3), L ins 1..4,
all_distinct(L), chain(L,#<), label(L).
Результат:
L = [1, 2, 3]
L = [1, 2, 4]
L = [1, 3, 4]
L = [2, 3, 4]
Рассмотрим теперь несколько примеров, иллюстрирующих ограничения сравнения.
Пример 1. Найти целочисленные решения системы неравенств
x>0, y>0, x – y <=1,
x + y <=4.
Программа:
:- use_module(library(clpfd)). main(L):-
L = [X, Y],
X#>0, Y#>0,
X - Y #=<1,
X + Y #=<4, label(L).
Запрос: main(L).
Вывод
L = [1, 1]
L = [1, 2]
36
L = [1, 3]
L = [2, 1]
L = [2, 2]
Полученные точки отмечены на рисунке кружками.
Пример 2. Выяснить, в какой из найденных точек достигается макси-
мум функции Z = X^2 – 2*X*Y + Y^2?
Решение.
:- use_module(library(clpfd)).
f(X, Y, Z):- Z #= X^2 – 2*X*Y + Y^2. main(X, Y, Z):-
X#>0, Y#>0,
X - Y #=<1,
X + Y #=<4, f(X,Y,Z),
label([X, Y, Z]).
Запрос:
findall(_Z, main(_X,_Y,_Z),_R), max_list(_R, Rmax), main(X, Y, Rmax),!.
Результат:
Rmax = 4,
X = 1,
Y = 3
Пример 3. Известно, что каждое натуральное число можно представить как сумму квадратов не более чем четырёх целых чисел. Получить все варианты представления для заданного числаN. Результат вывести по возрастанию слагаемых суммы.
Решение.
:- use_module(library(clpfd)). main(N,R):-
R=[X, Y, Z, U],
X#>=0, Y#>=0, Z#>=0, U#>=0, X#=<Y, Y#=<Z, Z#=<U, U#=<N, X^2 + Y^2 + Z^2 + U^2 #= N, label(R).
Для большинства N существует несколько решений. Чтобы получить их одним запросом, используем предикат fail, который всегда ложен:
main(53,Res),
write("Res="), writeln(Res), fail.
37
Результат:
Res=[0, 0, 2, 7]
Res=[0, 1, 4, 6]
Res=[2, 2, 3, 6]
Пример 4. Имеется неограниченное число монет номиналом 2 и 5 рублей. Необходимо заплатить этими монетами сумму S рублей (S > 0), отдав при этом минимальное число монет.
Решение.
:-use_module(library(clpfd)). main(S, K2, K5):-
K2#>=0, K5#>=0, 2*K2 + 5*K5 #= S, label([K2, K5]).
Но предикат main найдёт все решения, например, main(23, X, Y) последовательно выдаст
X = 4, Y = 3
X = 9, Y = 1
Составим новый запрос:
findall(S, (main(23, X, Y), S is X+Y), R), min_list(R,Rmin),
main(23, X, Y), X+Y =:= Rmin.
Тогда получим:
R = [7, 10],
Rmin = 7,
X = 4, Y = 3
Пример 5. Расставьте цифры от 1 до 9 без повторений в пустые кружки так, чтобы каждое число (кроме чисел в кружках верхнего уровня) являлось суммой чисел из связанных с ним вышестоящих значений. Кружки для чётных и нечётных цифр окрашены в разные цвета.
38
Решение. Так как белых кружков пять, они будут содержать нечётные цифры, а серые – чётные. Обозначим значения в пустых кружках первыми буквами латинского алфавита. Получим следующую картинку:
Напишем программу:
:-use_module(library(clpfd)). |
|
|
puzzle(L):- |
|
|
L = [A,B,C,D,E,F,G,H,I], |
|
|
A+B #= F, |
|
|
C+D #= I, |
|
|
E+F #= 11, |
|
|
G+H #= 13, |
|
|
I+11+13 #= 33, |
|
|
L ins 1..9, |
% встроенный предикат «все_разные» |
|
all_distinct(L), |
||
[A,D,F,H,I] ins 1\/3\/5\/7\/9, |
% объединение доменов |
|
[B,C,E,G] ins 2\/4\/6\/8, |
% объединение доменов |
|
label(L). |
|
|
и сформулируем цель (запрос):
?- puzzle(L).
Получим ответ:
L = [1, 2, 4, 5, 8, 3, 6, 7, 9]
39
Пример 6. Решить числовой ребус SEND + MORE = MONEY. Пример взят из документации SWI-Prolog и немного модифицирован.
Решение.
:- use_module(library(clpfd)).
main(SEND, MORE, MONEY):- |
+ N*10 |
+ D, |
|||||
SEND |
#= |
S*1000 |
+ |
E*100 |
|||
MORE |
#= |
M*1000 |
+ |
O*100 |
+ R*10 |
+ E, |
|
MONEY |
#= M*10000 |
+ |
O*1000 + N*100 + E*10 + Y, |
||||
SEND + MORE #= |
MONEY, |
|
|
||||
[E,N,D,O,R,Y] |
ins |
0..9, |
|
|
|||
[S,M] |
|
|
ins |
1..9, |
|
|
|
Ls = [S,E,N,D,M,O,R,Y], all_distinct(Ls), label(Ls).
Запрос:
?- main(SEND, MORE, MONEY).
Вывод:
MONEY = 10652,
MORE = 1085,
SEND = 9567
1.9. Запросы к базе данных
Набор фактов в Пролог-программе можно рассматривать как базу данных. Таблицы базы данных реализуются в виде предикатов. Запросы для извлечения информации из базы формулируются как цель программы. Покажем на нескольких примерах, как это делается.
Пример базы данных.
Пусть информация о возрасте и росте спортсменов собрана в следующую таблицу.
Имя |
Возраст |
Рост |
Игорь |
18 |
189 |
Роман |
17 |
182 |
|
|
|
Виктор |
20 |
185 |
Антон |
20 |
191 |
|
|
|
Николай |
16 |
193 |
|
40 |
|
