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

Функциональное и логическое программирование. Практикум

.pdf
Скачиваний:
0
Добавлен:
08.09.2026
Размер:
2 Мб
Скачать
☆
При быстрой сортировке сначала разбиваем список на два списка по разделителю – голове списка, затем упорядочиваем новые списки и соединяем их. Если новые списки получаются примерно одинаковой длины, то временная сложность алгоритма быстрой сортировки будет примерно nlogn. Если же длины списков сильно различаются, то сложность будет порядка n2. В среднем время быстрой сортировки ближе к лучшему случаю, чем к худшему. В программе будем использовать определенные в п.2.7.2.2 и 2.7.2.4 предикатов append и split.
domains
list=integer*
predicates
q_sort (list,list) append(list,list,list) split (integer,list,list,list)
clauses
q_sort([Head|Tail],Sort_list):-
split(Head,Tail,Less,More), q_sort(Less,Sort_less), q_sort(More,Sort_more), append(Sort_less,[Head|Sort_more],Sort_list).
q_sort([],[]).
2.7.4 Компоновка данных в список
Иногда при программировании на Прологе возникает необходимость
собрать данные из базы данных в список для их последующей обработки. Для этих целей в Прологе имеется встроенный предикат findall, обращение к которому имеет вид:
findall(X,P,L), где X – объект предиката P, L – имя переменной
выходного списка. Этот предикат формирует список из объектов X предиката P.
Пусть имеются сведения о количестве детей у определенных лиц (предикат data). Требуется найти общее количество детей в этой группе людей.
domains
family=symbol sum=integer kolvo=integer
list=kolvo*
predicates
data(family,kolvo) all(list,sum)
goal
findall(Kolvo,data(_,Kolvo),L), /*Элементы списка L – количества детей */ all(L,S),
write(“Общее количество детей: ”, S),nl.
71
clauses
data(иванов,2). ………………….
data(петров,1). all([],0). all([Head|Tail],S):-all(Tail,S1),S=S1+Head.
2.8 Решение логических задач с использованием списков
2.8.1 Задача о фермере, волке, козе и капусте
Рассмотрим широко известную логическую задачу о фермере, волке, козе и капусте. Задача заключается в следующем. Фермер (farmer), волк (wolf) , коза (goat) и капуста (cabbidge) находятся на одном берегу. Всем надо перебраться на другой берег на лодке. Лодка перевозит только двоих. Нельзя оставлять на одном берегу козу и капусту (коза съест капусту), козу и волка (волк съест козу).
Главная проблема в формировании алгоритма - найти эффективное представление структурой данных информации о задаче. Процесс перевозки может быть представлен последовательностью состояний state с 4 аргументами, каждый из которых может принимать два значения: right – на правом берегу, left – на левом берегу и отражает соответственно нахождение фермера, волка, козы и капусты. Например, state(left,right,left,right) означает, что фермер и коза находятся на левом берегу, а волк и капуста – на правом.
Фермер может перевести на другой берег кроме себя либо волка, либо козу, либо капусту. Для описания возможных вариантов перевозки (переходов из одного состояния в другое) напишем предикат move. Для того чтобы можно было переправляться с левого берега на правый и наоборот, определим предикат opposite, который определяет сторону, противоположную исходной.
move(state(X,X,G,C),state(Y,Y,G,C)):-opposite(X,Y). /* фермер + волк */ move(state(X,W,X,C),state(Y,W,Y,C)):-opposite(X,Y). /* фермер + коза */ move(state(X,W,G,X),state(Y,W,G,Y)):-opposite(X,Y). /*фермер + капуста */ move(state(X,W,G,C),state(Y,W,G,C)):-opposite(X,Y). /* только фермер */ opposite(right,left). opposite(left,right).
С помощью предиката danger определим опасные состояния, когда коза может съесть капусту или волк съесть козу.
danger(state(F,_,X,X)):-opposite(F,X). /*коза может съесть капусту*/ danger(state(F,X,X,_)):-opposite(F,X)./*волк может съесть козу*/
Наконец, определим предикат, который path, который сформирует список из последовательности состояний, решающих поставленную задачу. Добавляем в список новое состояние, если в него возможен переход из текущего состояния, оно не опасно и не содержится в уже сформированной последовательности состояний (чтобы не было зацикливания)
path(G,G,T,T):-!.
72
path(S,G,L,L1):-move(S,S1), not(danger(S1) ), not(member(S1,L)), path(S1,G,[S1|L],L1),!.
Итак, окончательный вариант программы будет иметь вид:
domains
state=state(symbol,symbol,symbol,symbol)
spisok=state*
predicates
move(state,state) opposite(symbol,symbol) danger(state) path(state,state,spisok,spisok)
member(state,spisok)
goal
S=state(left,left,left,left), G=state(right,right,right,right), path(S,G,[S],L), nl,write('A solution is:'),nl, write(L).
clauses
move(state(X,X,G,C),state(Y,Y,G,C)):-opposite(X,Y). move(state(X,W,X,C),state(Y,W,Y,C)):-opposite(X,Y). move(state(X,W,G,X),state(Y,W,G,Y)):-opposite(X,Y). move(state(X,W,G,C),state(Y,W,G,C)):-opposite(X,Y). opposite(right,left).
opposite(left,right). danger(state(F,_,X,X)):-opposite(F,X). danger(state(F,X,X,_)):-opposite(F,X). path(G,G,T,T):-!. path(S,G,L,L1):- move(S,S1), not(danger(S1) ), not(member(S1,L)), path(S1,G,[S1|L],L1),!. member(X,[X|_]). member(X,[_|L]):-member(X,L).
Пролог найдет следующее решение задачи:
[state(left,left,left,left), state(right,left,right,left), state(left,left,right,left), state(right,left,right,right), state(left,left,left,right), state(right,right,left,right), state(left,right,left,right), state(right,right,right,right)]
Это соответствует следующему алгоритму:
1. Фермер перевозит козу с левого берега на правый.
2. Фермер перемещается с правого берега на левый.
3. Фермер перевозит капусту с левого берега на правый.
4. Фермер перевозит козу с правого берега на левый.
73
5. Фермер перевозит волка с левого берега на правый.
+
6. Фермер перемещается с правого берега на левый.
7. Фермер перевозит козу с левого берега на правый.
2.8.2 Решение числовых ребусов
Рассмотрим пример числового ребуса:
DONALD GERALD ROBERT
Задача состоит в том, чтобы заменить все буквы цифрами. Одинаковым буквам должны соответствовать одинаковые цифры, а разным буквам – разные цифры.
Определим предикат sum(N1,N2,N), который истинен, если существует такая замена букв цифрами в словах N1, N2, N, что N1+N2=N. Числа будем представлять списком цифр, из которых состоит число. Без ограничения общности можно считать, что все три списка, представляющие числа имеют равную длину. Если числа разной длины, то всегда можно приписать нужное количество нулей более «коротким» числам. В правилах для предиката sum необходимо описать правила суммирования в десятичной системе счисления. Суммирование производится слева направо с учетом цифры переноса с предыдущего разряда. Следовательно, при суммировании очередных цифр нужна дополнительная информация о переносе из предыдущего разряда и о переносе в следующий разряд. Поскольку разным буквам должны соответствовать разные цифры, то при суммировании цифр нужна информация о цифрах, доступных до и после сложения (эта информация будет представляться двумя списками). В связи с вышесказанным определим вспомогательный предикат sum1 с дополнительными параметрами: sum1(N1,N2,N, P_before,P_after, Cifri_before,Cifri_after), где P_before – перенос из правого разряда до сложения, P_after - перенос в левый разряд после сложения, Cifri_before – список цифр, которые могут быть использованы для конкретизации переменных до сложения, Cifri_after – список цифр, которые могут быть использованы для конкретизации переменных после сложения. Тогда можно описать связь предиката sum и sum1 следующим образом:
sum(N1,N2,N):-sum1(N1,N2,N,0,0,[0,1,2,3,4,5,6,7,8,9],_).
Определим отношение sum1 следующим образом:
Если у всех трех чисел имеется хотя бы одна цифра (в списках можно
выделить головы), то суммируем числа без самой левой цифры и, используя список оставшихся цифр и перенос из предыдущего разряда, суммируем самые левые цифры и добавляем полученную сумму в результат суммирования без самой левой цифры. Для суммирования цифр определим предикат sumc.
Если все три списка пустые, то переносов разрядов нет, и списки цифр до и
после сложения совпадают. Это соответствует правилу остановки рекурсии:
74
sum1([],[],[],0,0,Cifri,Cifri).
Получаем следующие правила:
sum1([],[],[],0,0,L,L). sum1([D1|N1],[D2|N2],[D|N],C1,C,L1,L):-
sum1(N1,N2,N,C1,C2,L1,L2), sumc(D1,D2,D,C2,C,L2,L).
Осталось определить правила для предиката sumc. Первые три аргумента предиката должны быть цифрами. Если какая-нибудь из этих переменных не конкретизирована, то ее необходимо конкретизировать какой-нибудь цифрой из списка L2. После конкретизации цифру следует удалить из списка доступных цифр. Если переменная уже имела значение, то удалять из списка доступных цифр ничего не надо.
sumc(D1,D2,D,C2,C,L2,L):-
delete(D1,L2,L3), delete(D2,L3,L4), delete(D,L4,L), S=D1+D2+C2, D=S mod 10, C=S div 10.
Для удаления цифры из списка или для получения значения неозначенной переменной будем использовать предикат delete. Для проверки означенности переменной будет использоваться встроенный предикат bound(<переменная>).
delete(X,L,L):-bound(X),!. delete(X,[X|L],L). delete(X,[Y|L],[Y|L1]):-delete(X,L,L1).
Итак, окончательный вариант программы будет иметь вид:
domains
list=integer*
predicates
sum(list,list,list) sum1(list,list,list,integer,integer,list,list) sumc(integer,integer,integer,integer,integer,list,list) delete(integer,list,list)
clauses
sum(N1,N2,N):-sum1(N1,N2,N,0,0,[0,1,2,3,4,5,6,7,8,9],_). sum1([],[],[],0,0,L,L). sum1([D1|N1],[D2|N2],[D|N],C1,C,L1,L):-sum1(N1,N2,N,C1,C2,L1,L2),
sumc(D1,D2,D,C2,C,L2,L).
sumc(D1,D2,D,C2,C,L2,L):-delete(D1,L2,L3),
delete(D2,L3,L4), delete(D,L4,L), S=D1+D2+C2, D=S mod 10, C=S div 10.
75
delete(X,L,L):-bound(X),!. delete(X,[X|L],L). delete(X,[Y|L],[Y|L1]):-delete(X,L,L1).
Для решения заданного в начале пункта ребуса следует ввести цель:
sum([D,O,N,A,L,D],[G,E,R,A,L,D],[R,O,B,E,R,T])
Для данного ребуса получим единственное решение: D=5, O=2, N=6, A=4, L=8, G=1, E=9, R=7, B=3, T=0.
2.9 Строки
Строкой называется последовательность символов или их ASCII кодов, заключенный в кавычки. Перед каждым ASCII кодом должен стоять знак обратного слэша (\).
Замечание Если строка вводится с клавиатуры, то кавычки не ставятся.
Пример 1: “ABC” – строка;
“\65\66\67”- та же самая строка в ASCII кодах.
Для строк определены следующие предикаты:
1. str_len (<строка>, <длина строки>) – находит длину строки.
Пример 2: При задании цели str_len (“Turbo Prolog”,N) переменная N получит значение, равное 12 (длина строки “Turbo Prolog”).
Пример 3: При задании цели str_len (“Turbo Prolog”,10) будет неуспешное завершение предиката (предикат будет сравнивать числа 12 и 10).
2. concat (<строка1>, <строка2>, <строка_результат>) – соединение
(конкатенация) строки1 и строки2.
Пример 4: При задании цели concat (“Turbo ”,”Prolog”, L) переменная L получит значение “Turbo Prolog”.
Заметим, что предикат concat можно использовать и для разбиения
строки.
Пример 5: При задании цели concat (“Turbo ”, L, ”Turbo Prolog”) переменная L получит значение “Prolog”.
3. frontstr (<кол-во символов>, <строка>, <подстрока>, <остаток строки>) –
находит подстроку из указанного количества первых символов строки.
Пример 6: При задании цели frontstr (6, “Turbo Prolog”,Substring, Rest_string) переменная Substring
получит значение “Turbo ”, а переменная Rest_string получит значение “Prolog”.
4. frontchar (<строка>, <символ>, <остаток строки>) – разбивает строку на
символьный префикс и оставшуюся часть строки. Символьный префикс заключается в апострофы.
Пример 7: Напишем предикат, преобразующий строку в список символов. Декларативное описание предиката: первый символ строки является головой
76
списка, хвост списка получается из подстроки без первого символа строки; если в строке нет символов, то список пуст.
domains
list=char*
predicates
string_listc(string,list)
clauses
string_listc(“”,[]). string_listc(Str,[Head|Tail]):- frontchar(Str,Head,Str1),
string_listc(Str1,Tail).
5. fronttoken(<строка>, <атом>, <остаток строки>) – выделяет в строке атом.
Атом (token) - особый вид строки. Атомами являются: последовательности
из букв, цифр и символа подчеркивания, начинающиеся с букв; специальные символы #, $, ! и т.д.; числа.
Пример 8: Напишем предикат, преобразующий строку в список атомов.
Декларативное описание предиката: первый атом в строке является головой списка, хвост списка получается из подстроки без первого атома строки; если в строке нет атомов (но могут остаться пробелы), то список пуст.
domains
list=symbol*
predicates
string_listt(string,list)
clauses
string_listt(Str,[Head|Tail]):-fronttoken(Str,Head,Str1),!,
string_listt(Str1,Tail).
string_listt(_,[]).
Пример 9: Напишем предикат, преобразующий строку в список атомов c
функторами. Такой предикат может использоваться для записи в базу данных фактов, относящихся к некоторому одноместному предикату, объектами которого могут быть атомы из строки символов. Пусть в качестве функтора выступает имя предиката женщина, а строка состоит из имен женщин.
domains
fact=женщина(symbol)
list=fact*
predicates
string_listf(string,list)
create_fact(symbol,fact)
clauses
string_listf(Str,[Head|Tail]):- fronttoken(Str,Token,Str1),!, create_fact(Token,Head),
string_listf(Str1,Tail).
string_listf(_,[]).
77
create_fact(Token,женщина(Token)).
Для цели string_ listf(“энн пат лиз джейн”, L) получим решение:
L=[женщина(энн),женщина(пат),женщина(лиз),женщина(джейн)]
2.10 Предикаты для работы с файлами
Для работы с файлами в разделе domains следует описать файловый
домен: file=<имя домена>. Имя домена – это логическое имя файла. Если в программе используется несколько файлов, то они перечисляются через точку с запятой.
Пример 1: domains file=data1;data2;data3
В каждый момент времени активными могут быть только два файла: один
для ввода и один для вывода. По умолчанию ввод осуществляется с клавиатуры (логическое имя keyboard), а вывод – на экран (логическое имя screen).
Предикат existfile(<имя физического файла>) успешно завершается, если
файл с указанным именем существует.
Для работы с файлом его открывают для чтения, записи или добавления с
помощью одного из предикатов:
openread(<логическое имя>,<имя физического файла >); openwrite(<логическое имя>,< имя физического файла >); openappend(<логическое имя>,< имя физического файла >).
Логическое имя файла объявляется в описании домена file, оно должно
начинаться со строчной буквы. Физическое имя файла должно быть строкой, т.е. заключаться в кавычки. При открытии файла происходит связывание логического имени с физическим файлом. При открытии файла предикатом openwrite, файл создается заново.
Для чтения и записи в файл используются предикаты из пункта 2.3.2.
Обработка файлов осуществляется последовательно. Следует иметь в виду, что предикаты для работы с файлами являются внелогическими, т.к. не дают альтернативных решений при откате (повторно считать то же самое из файла в случае неуспешной обработки данных нельзя). Поэтому чтение из файла и обработка считанного должна производиться отдельно!
Пример 2 Пусть целые числа вводятся с клавиатуры и на экран выводятся из квадраты. Завершение ввода – ввод нуля. Рассмотрим вариант программы, когда чтение и обработка данных не разделены.
predicates
sqr
goal
write(“Программа находит квадраты чисел, введенных с клавиатуры. Окончание ввода – 0”),nl,
sqr.
clauses sqr:- write(“Введите целое число “),
78
readint(N),N=0,!.
sqr:- write(“Введите целое число “),
readint(N), N2=N*N, write(“Квадрат числа “,N,” равен “,N2),nl, sqr.
Если с клавиатуры ввести числа 5, 3, 6, 4, 0, то на экране появятся только квадраты чисел 3, 4. Квадраты чисел 5, 6 не вычисляются. Более того, если ввод 0 будет на четном месте во вводимой последовательности, то остановки ввода не произойдет. Правильный вариант программы с разделением ввода и обработки данных будет иметь вид:
predicates
sqr process(integer)
goal
write(“Программа находит квадраты чисел, введенных с клавиатуры. Окончание ввода – 0”),nl,
sqr.
clauses
sqr:- write(“Введите целое число “),
readint(N),
process(N). process(0):-!. process(N):-N2=N*N, write(“Квадрат числа “,N,” равен “,N2),nl, sqr.
Предикат eof(<логическое имя>) успешно завершается, если найден конец
файла.
После окончания работы с файлом его закрывают с помощью предиката
closefile(<логическое имя>).
Для перенаправления ввода или вывода используют предикаты:
readdevice(<логическое имя>), writedevice(<логическое имя>).
Находясь в окне редактора с помощью клавиши F8 можно открыть второе окно редактора для просмотра и редактирования файлов. Закрытие этого окна – клавиша Esc.
Пример 3: Напишем предикат write_to_file, который записывает вводимый с клавиатуры текст в файл t.txt. Окончание ввода – символ #.
domains file=myfile predicates
write_to_file write_char(char)
goal
79
write(“Введите текст для записи в файл t.txt \n Окончание ввода – #”), nl, write_to_file.
clauses
write_to_file:-readchar(X), /* Вводимый символ на экране не отображается */
write(X), /* Вывод символа на экран*/ openwrite(myfile,”t.txt”), /* Открытие файла для записи*/ writedevice(myfile), /* Перенаправление вывода в файл*/ write_char(X), /* Запись символа в файл */ closefile(myfile), /* Закрытие файла*/ writedevice(screen), /* Перенаправление вывода на экран*/ nl,write(“Данные записаны в файл”),nl. write_char(‘#’). write_char(‘\13’):­ nl, /* Если нажата клавиша Enter, то переход на следующую строку в файле*/ writedevice(screen), /* Перенаправление вывода на экран*/ nl, /* Переход на следующую строку на экране*/ readchar(X), /* Ввод следующего символа*/ write(X), /* Вывод символа на экран*/ writedevice(myfile), /* Перенаправление вывода в файл*/ write_char(X). /* Продолжение ввода*/ write_char(X):-write(X), /* Вывод символа в файл*/ writedevice(screen), /* Перенаправление вывода на экран*/ readchar(Y), /* Ввод следующего символа*/ write(Y), /* Вывод символа на экран*/ writedevice(myfile), /* Перенаправление вывода в файл*/ write_char(Y). /* Запись символа в файл */
Пример 4: Напишем предикат add_to_file, который записывает вводимые
с клавиатуры строки в файл, имя которого вводится с клавиатуры. Причем, если файл уже существовал, введенные строки добавляются в конец файла. Окончание ввода – строка “end”.
domains file=myfile predicates
add_to_file write_string(string) check_exist(string)
goal
add_to_file.
clauses
add_to_file:-write(“Введите имя файла: “), readln(Filename), /* Ввод с клавиатуры имени файла */ check_exist(Filename), /* Проверка существования файла*/ write(“Введите строки для добавления в файл”),nl,
80
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]