Язык программирования SWI-Prolog. Учебно-методическое пособие
.pdfRes=[('Петр',P),('Роман',R),('Сергей',S)], v1(P,S),
v2(P,R), v3(R,S).
Результат выполнения запроса solve(Res):
R = [('Петр',хим), ('Роман',физ), ('Сергей',мат)]
То есть Петр – химик, Роман – физик, а Сергей – математик.
1.11. Игры и головоломки
Задача 1. На доске размером 5×5 расставить 5 ферзей так, чтобы они не атаковали друг друга. Для этого на каждой горизонтали, вертикали и диагонали должно стоять не более одного ферзя.
Решение будем искать в виде списка, k-тый элемент которого есть номер вертикали, в которой стоит ферзь, размещаемый на k-той горизонтали. Все элементы списка должны быть различными, тогда на каждой горизонтали и вертикали будет находиться по одному ферзю. Заметим также, что два ферзя не стоят на одной диагонали, если суммы и разности номеров вертикали и горизонтали у них не равны.
% расстановка ферзей
:-use_module(library(clpfd)). place(V):-
V = [V1, V2, V3, V4, V5],
Vins 1..5, all_distinct(V),
%S - суммы, R - разности
S1 #= 1+V1, R1 #= 1-V1,
S2 #= 2+V2, R2 #= 2-V2,
S3 #= 3+V3, R3 #= 3-V3,
S4 #= 4+V4, R4 #= 4-V4,
S5 #= 5+V5, R5 #= 5-V5,
S= [S1,S2,S3,S4,S5],all_distinct(S),
R= [R1,R2,R3,R4,R5],all_distinct(R), label(V).
Запрос: place(V).
Результат (10 вариантов):
V = [1, 3, 5, 2, 4]
V = [1, 4, 2, 5, 3]
51
V = [2, 4, 1, 3, 5]
V = [2, 5, 3, 1, 4]
V = [3, 1, 4, 2, 5]
V = [3, 5, 2, 4, 1]
V = [4, 1, 3, 5, 2]
V = [4, 2, 5, 3, 1]
V = [5, 2, 4, 1, 3]
V = [5, 3, 1, 4, 2]
Если в начало программы добавить директиву:- use_rendering(chess)., то результат будет представлен рисунком шахматной доски с расставленными на ней ферзями (на рисунке представлен только первый вариант решения).
Задача 2. Ханойские башни – это игра, в которой используются три штыря и набор дисков. Все диски различаются диаметром и нанизываются на штыри через отверстие в центре каждого диска. Первоначально все диски находятся на левом штыре. Цель игры состоит в том, чтобы переместить все диски на центральный штырь. Правый штырь можно использовать как «запасной» для временного размещения дисков. При каждом пе-
ремещении диска с одного штыря на другой должны соблюдаться два ограничения: перемещать можно только самый верхний диск на штыре и, кроме того, нельзя ставить диск на другой диск меньшего размера.
Решение. Обозначим штыри слева направо буквамиA, B, C. Напишем предикаты «переместить» и «сообщить»:
переместить(0,_,_,_):-!.
переместить(N, А,В,С):-
М is N-1,
переместить(М,А,С,В), сообщить(А,В), переместить(М,С,В,А).
сообщить(X,Y):-
write('переместили диск со штыря '), write(X), write(' на штырь '), writeln(Y).
Пример запроса: переместить(3, 'A','B','C').
Результат:
переместили диск со штыря A на штырь B переместили диск со штыря A на штырь C
52
переместили диск со штыря B переместили диск со штыря A переместили диск со штыря C переместили диск со штыря C переместили диск со штыря A
на штырь C на штырь B на штырь A на штырь B на штырь B
Задача 3. Судоку. Игровое поле популярной в настоящее время игры представляет собой квадрат размером 9×9, разделённый на меньшие квадраты со стороной в 3 клетки. Таким образом, всё игровое поле состоит из 81 клетки. В них уже в начале игры стоят некоторые числа из диапазона от 1 до 9. Требуется заполнить свободные клетки цифрами из того же диапазона так, чтобы в каждой строке, в каждом столбце и в каждом выделенном квадрате 3×3 каждая цифра встречалась бы только один раз. Решение головоломки требует анализа позиции, логических рассуждений, а иногда и частичного перебора вариантов. Задача легко решается с помощью программы на Прологе, приведенной в документации кSWI-Prolog. Мы рассмотрим совсем простой вариант игры– судоку размером 4×4, для решения которой напишем собственную программу.
:- use_module(library(clpfd)). vals(S):- S ins 1..4. sudoku(Rows):-
Rows=[S1,S2,S3,S4], maplist(vals, Rows), maplist(all_distinct, Rows), transpose(Rows,Columns), maplist(all_distinct, Columns), blocks(S1,S2), blocks(S3,S4).
blocks(A,B):- A=[A1,A2,A3,A4], B=[B1,B2,B3,B4],
all_distinct([A1,A2,B1,B2]), all_distinct([A3,A4,B3,B4]).
problem(1,[[_,_,1,_], [_,_,_,2], [_,3,_,_], [_,_,_,4]]).
Запрос:
problem(1,_Rows), sudoku(_Rows), maplist(writeln,_Rows).
53
Результат:
[2, 4, 1, 3] [3, 1, 4, 2] [4, 3, 2, 1] [1, 2, 3, 4]
Пояснения. Вспомогательный предикат vals(S):- S ins 1..4. используем, чтобы задать ограничения на элементы судоку. Размеры матрицы судоку можно не задавать, так как они определены в предикате problem. Предикат sudoku получает набор Rows из четырёх строк:
Rows=[S1,S2,S3,S4],
Для каждой строки с помощьюmaplist(vals, Rows) задаются ограничения на значения элементов(диапазон 1..4). Аналогично задаётся ограничение «все разные» для элементов каждой строки:
maplist(vals, Rows), maplist(all_distinct, Rows),
Затем из набора строк стандартный предикатtranspose формирует набор столбцов (а фактически транспонирует матрицу игры, чтобы получить столбцы). Для каждого столбца проверяется ограничение «все разные»:
transpose(Rows,Columns), maplist(all_distinct, Columns),
Осталось задать ограничение «все разные» для блоков размера 2×2. Изза небольшого размера судоку это можно сделать «в лоб» без использования рекурсии.
blocks(A,B):- A=[A1,A2,A3,A4], B=[B1,B2,B3,B4],
all_distinct([A1,A2,B1,B2]), all_distinct([A3,A4,B3,B4]).
Задача 4. Старинная головоломка «Волк, коза и капуста». Однажды крестьянину понадобилось перевезти через реку волка, козу и капусту. У крестьянина есть лодка, в которой может поместиться, кроме самого крестьянина, только один объект – или волк, или коза, или капуста. Если крестьянин оставит без присмотра волка с козой, то волк съест козу; если крестьянин оставит без присмотра козу с капустой, коза съест капусту.
Как крестьянину перевезти на другой берег всё своё имущество в -це лости и сохранности?
54
Решение. Информационная модель задачи включает в себя пространство состояний и правила перехода из одного состояния в другое. Известно также начальное и конечное состояние. Необходимо найти цепочку переходов из начального состояния в конечное.
Представим отдельное состояние тройкой переменных Волк, Коза, Капуста. Значения переменных:
0 – объект находится на исходном берегу;
1 – объект находится на целевом берегу.
Таким образом, начальному состоянию задачи будет соответствовать тройка (0, 0, 0), а конечному – тройка (1, 1, 1). Для изменения состояния напишем предикат change, в котором реализуем проверку допустимости перехода. Параметрами предиката является текущее состояние– тройка значений Волк1, Коза1, Капуста1 и следующее состояние – тройка значений Волк2, Коза2, Капуста2.
Пусть крестьянин перевозит волка с одного берега на другой, тогда значение соответствующей переменной должно измениться с0 на 1 или с 1 на 0. Это можно реализовать формулой Волк2 is 1-Волк1. При этом значения остальных переменных не изменятся: Коза2=Коза1, Капуста2=Капуста1. Заметим, что такой переход возможен, если коза не останется вместе с капустой на одном и том же берегу. Поэтому в предикат включим условие Коза1\=Капуста1.
В итоге получим:
change(Волк1,Коза1,Капуста1,Волк2,Коза2,Капуста2):- Волк2 is 1-Волк1,
Коза1\=Капуста1, Коза2=Коза1, Капуста2=Капуста1.
Аналогично при перевозке капусты нужно проверять, что волк и коза не останутся вместе на одном и том же берегу:
change(Волк1,Коза1,Капуста1,Волк2,Коза2,Капуста2):- Капуста2 is 1-Капуста1,
Волк1\=Коза1, Волк2=Волк1, Коза2=Коза1.
И только козу можно перевозить без дополнительной проверки:
change(Волк1,Коза1,Капуста1,Волк2,Коза2,Капуста2):- Коза2 is 1-Коза1,
Волк2=Волк1, Капуста2=Капуста1.
55
Проверку конечного состояния вынесем в отдельный предикат:
finish(Волк,Коза,Капуста):- Волк=1, Коза=1, Капуста=1.
Решение задачи осуществляет предикатsolve, который получает текущее состояние и список уже пройденных состояний. Список пройденных состояний позволяет предотвратить зацикливание процесса поиска, так как исключает повторение одних и тех же состояний.
solve(Волк0,Коза0,Капуста0,Список):-
%переходим в следующее допустимое состояние change(Волк0,Коза0,Капуста0,Волк,Коза,Капуста),
%проверяем, является ли состояние конечным finish(Волк,Коза,Капуста),
%если да, то выводим его writeln((Волк,Коза,Капуста)),!;
%если нет, то получаем то же самое следующее состояние change(Волк0,Коза0,Капуста0,Волк,Коза,Капуста),
%если его нет в списке пройденных
not(member((Волк,Коза,Капуста),Список)), % то выводим его writeln((Волк,Коза,Капуста)),
%и добавляем в список пройденных Список1=[(Волк,Коза,Капуста)|Список],
%затем выполняем рекурсивный вызов предиката solve(Волк,Коза,Капуста,Список1).
Заметим, что повторное получение следующего состояния необходимо, так как в случае ложности предикатаfinish не будет выполнена ни одна команда всей первой ветви!
Чтобы получить решение, напишем запрос: solve(0,0,0,[(0,0,0)]),!. Результат (для наглядности в скобках дана расшифровка перехода):
0,1,0 (перевозим козу)
1,1,0 (перевозим волка)
1,0,0 (возвращаем козу)
1,0,1 (перевозим капусту)
1,1,1 (перевозим козу)
Задача 5. Поиск в пространстве состояний. Поиск решения в предыдущей задаче опирался на встроенный в Пролог механизм бэктрекинга. Алгоритмическая модель этого поиска соответствует поиску в глубину на
56
ориентированном графе. Такая модель работоспособна только в том случае, если пространство состояние конечно, но и в этом случае решение не обязательно будет оптимальным.
Вслучае бесконечного множества состояний нужно использовать поиск «в ширину», при котором сначала просматриваются состояния, в которые можно перейти из начального за один шаг, затем за два шага, затем за три шага и т. д. В этом случае найденное решение(т. е. конечное состояние) будет получено за наименьшее число шагов.
Покажем технику использования этого метода на примере следующей задачи. За один ход разрешается или удваивать число, или стирать его последнюю цифру. Необходимо за наименьшее число ходов получить из числа 458 число 14.
Впроцессе решения мы будем строить кратчайшие пути из начального состояния во все остальные, пока не попадём в конечное состояние. Необходимая для этого структура данных– это список подсписков. Создадим сначала список из одного подсписка с начальным значением: L = [[458]]. Два следующих состояния – это значения 2*458 = 916 и 458 div 10 = 45. Добавим
вконец основного списка два новых подсписка: [916, 458] и [45, 458] и исключим уже не нужный подсписок[458]. Теперь список L будет содержать пути к состояниям, доступным за один шаг. Сделаем ещё один шаг. Берём первое значение в каждом подсписке (текущее состояние) и генерируем пути от этих значений к следующим. Каждую пару новых путей добавляем в конец основного списка и удаляем уже не нужный путь к текущему состоянию. Получим список путей к состояниям, доступным за два шага:
L = [[1832, 916, 458], [91, 916, 458], [90, 45, 458], [4, 45, 458]]
Так как ни одно из полученных новых значений(выделены полужирным шрифтом) не равно 14, алгоритм продолжит свою работу. Как только мы получим значение 14, мы получим и путь к этому значению.
Приведем программу поиска путей.
path([H|T],R):- H=[X|_],
( X=14, R=H,!; % достигнуто конечное состояние % --- иначе
Y1 is 2*X, Y2 is X div 10,
W1=[Y1|H], W2=[Y2|H], append(T,[W1,W2],L1), path(L1,R)
).
57
Запрос: L=[[458]], path(L,_R), reverse(_R,R).
Результат:
L = [[458]],
R = [458, 916, 1832, 3664, 7328, 14656, 1465, 146, 14]
Так как в очередь сначала добавляются удвоенные значения, в процессе решения мы получили большие числа. Поменяем порядок добавления подсписков в очередь (заменим выделенный фрагмент программы):
Y1 is X div 10, Y2 is 2*X,
Получим другое решение задачи:
L = [[458]],
R = [458, 45, 90, 9, 18, 36, 72, 7, 14]
Количество шагов осталось прежним, изменился только путь от начального состояния к конечному.
1.12. Организация диалога с пользователем
Запрос данных в процессе выполнения программы осуществляет оператор read(Имя), где параметр «Имя» есть имя переменной. Оператор может вводить символы, числа, списки, т. е. любые термы. Для вывода значения переменной можно пользоваться операторами write(Имя) – вывод без перевода строки, или writeln(Имя) – вывод с переводом строки. Тем же оператором можно вывести строку символов. Заметим, что в Прологе есть отдельный оператор перевода строки – оператор nl (сокращение от new line).
Пример предиката:
test(R):-
writeln('Число 34 в двоичной системе счисления равно'), writeln('1: 100010'),
writeln('2: 111000'), writeln('3: 101010'),
prompt(_,'Введите номер правильного ответа'), read(N),
N = 1 > R = true; R = false.
При запуске предиката на экран будет выведено:
58
В поле ввода, в котором необходимо ввести номер ответа, появится приглашение к вводу, текст которого задаётся в оператореprompt. После ввода числа необходимо нажать кнопку Send или клавишу Enter.
1.13. Отладка программы
Пролог-программу отлаживать достаточно трудно.
Основные возможности: задание точек останова, трассировка программы. Также можно расширить информацию об ошибках, указав в начале программы опцию :- debug.
Мощным средством выявления ошибок служит также добавление в текст программы оператора throw для обработки исключительных ситуаций.
Пример. Напишем предикат
pr_1(X,Y):- Y is sqrt(X).
и выполним запрос pr_1(-4,Y).
Поскольку квадратный корень из отрицательного числа не извлекается, появится сообщение об ошибке:
Arithmetic: evaluation error: `undefined' In:
[1] _1392 is sqrt(-4)
Из сообщения видно, что Пролог не может вычислить значение переменной Y, в результате она не создаётся. Но на самом деле ошибка состоит
в том, что значение аргумента не принадлежит области определения функции sqrt, а это ошибка типаdomain_error. Добавим в предикат обработку этой ошибки:
pr_2(X,Y):-
X>=0, Y is sqrt(X),!; throw(error(domain_error('X>=0',X),_)).
Тогда при выполнении запроса pr_2(-4,Y) появится другое сообщение:
Domain error: 'X>=0' expected, found '-4' In:
[1] throw(error(domain_error('X>=0',-4),_1400))
Заметим, что бóльшая часть сообщений адекватно отражает суть ошибки. Например, при выполнении запроса pr_2(X,Y) обнаруживается,
что переменная X не инициализирована:
59
Arguments are not sufficiently instantiated In:
[2] _1410>=0
[1] pr_2(_1442,_1444) at line 1
Приведём ещё несколько сообщений:
Syntax error: Operator expected – ожидается оператор.
Out of local stack – переполнение стека. Чаще всего возникает в случае бесконечной рекурсии.
Syntax error: Unexpected end of file – неожиданный конец файла. Singleton variables [список переменных] – предупреждение: «одноразо-
вые» переменные. Чаще всего это параметр предиката, который не используется в теле предиката. Для исправления ошибки данный параметр заменяется анонимной переменной.
Singleton variable in branch – та же ошибка, но другой локализации. Procedure ’...’ does not exist – процедура ’...’ не существует (не опреде-
лена).
No permission to redefine built-in predicate – нельзя переопределить встроенную процедуру.
Отладочный оператор trace позволяет выполнять программу по шагам. Пример отладки:
Задача. Вычислить сумму кубов первых N натуральных чисел. Решение.
sum_cubes(0,0):-!. sum_cubes(N,R):-
N1 is N-1,sum_cubes(N1,R1), R is R1+N^3.
Запрос: trace, sum_cubes(2,Res).
При выполнении запроса можно выбрать один из вариантов выполнения с помощью кнопок с пиктограммами:
Перечислим их действия в порядке слева направо: Continue – продолжить выполнение программы; Step into – шаг с заходом в подпрограммы;
60
