Программирование в среде Visual Prolog. Учебное пособие
.pdfneighbors(X,Y,L):–
conc(_,[X,Y|_],L). /* список L получается путем объединения некоторого списка со списком, голову которого составляют элементы X и Y */
Обратите внимание, что этот предикат проверяет только наличие нужных значений в указанном порядке. Если нам не важен порядок,
вкоторомдваданныхзначениявстречаютсявнекоторомсписке, тоследуетзаписатьмодификациюописанноговышепредиката, котораябудет проверять оба варианта размещения искомых элементов. Для этого достаточно, чтобысписокраскладывалсянадваподсписка, причемголова второго подсписка содержала два наших элемента либо в прямом, либо
вобратном порядке. Соответствующий программный код будет следующим:
neighbors2(X,Y,L):– conc(_,[X,Y|_],L); conc(_,[Y,X|_],L). /*
список L получается
путем объединения некоторого списка со списком, голову которого составляют элементы X и Y или элементы Y и X */
3. Сортировка списка
Под сортировкой обычно понимают расстановку элементов в некотором порядке. Для определенности будем упорядочивать элементы списков по неубыванию: если сравнить любые два соседних элемента списка, то следующий должен быть не меньше предыдущего.
Рассмотрим сортировку списка методом «пузырька». Его еще называют методом прямого обмена или методом простого обмена.
Идея этого метода заключается в следующем. На каждом шаге сравниваются два соседних элемента списка. Если оказывается, что они стоят неправильно, т. е. предыдущий элемент меньше следующего, то они меняются местами. Этот процесс продолжаем до тех пор, пока есть парысоседнихэлементов, расположенныхвнеправильномпорядке. Это и будет означать, что список отсортирован. Аналогия с пузырьком вызвана тем, что при каждом проходе минимальные элементы как бы «всплывают» к началу списка.
11
Реализуем пузырьковую сортировку с помощью двух предикатов. Первый, назовемегоpermutation, будетсравниватьдвапервых элемента списка и если первый окажется больше второго, менять их местами. Если же первая пара расположена в правильном порядке, этот предикат будетпереходитькрассмотрениюхвоста. Фактическиэтотпредикатбудет осуществлять один проход по списку и переставлять максимальный элемент в конец списка. Основной предикат bubble будет вызывать вспомогательный предикат permutation до тех пор, пока весь список не будет отсортирован.
permutation([X,Y|T],[Y,X|T]):–
X>Y,!.
/* переставляем первые два элемента, если первый больше второго */
permutation([X|T],[X|T1]):– permutation(T,T1).
/* иначеоставляемпервыйэлементнаместеипереходим к перестановкам в хвосте*/
bubble(L,L1):–
permutation(L,LL), /* вызываем предикат для одного прохождения по списку */
!,
bubble(LL,L1). /* пытаемся еще раз отсортировать полученный список */
bubble(L,L). /* если перестановок не было, значит, список отсортирован */
Первоеправилоbubble работаеттолькодотехпор, покаестьхотябы пара элементов списка, расположенных в неправильном порядке. Как только такие элементы закончились, предикат permutation терпит неудачу, а bubble переходит от правила к факту и возвращает в качестве второго аргумента отсортированный список.
При формировании списка из данных, содержащихся в виде фактов вбазеданныхилинепосредственновпрограмме, полезнымоказывается стандартный предикат findall. Он собирает значения, получаемые в процессе бектрекинга, в список. Формат предиката:
findall(Переменная, Предикат, Список),
12
где первый аргумент определяет параметр, который необходимо собрать в список, второй– определяет предикат, из которого формируется список значений, третий – формируемый список.
Например, пусть имеется программа:
domains
list =symbol* name=symbol age=integer
predicates men(name, age)
clauses
men(brain, 17). men(maikl, 18). men(bill, 18). men(stiv, 16). men(bob,18).
Теперь, если необходимо сформировать список мужчин призывного возраста, то необходимо указать цель:
goal
findall(N, men(N, 18), L)
В результате получим списокL=[maikl, brain, bob ].
Задания к практической работе
1.Написать программу, определяющую основные отношения на множествах:
а) ОБЪЕДИНЕНИЕ; б) ПЕРЕСЕЧЕНИЕ; в) ВЫЧИТАНИЕ;
г) ДЕКАРТОВО ПРОИЗВЕДЕНИЕ.
2.Написать программу, которая вставляет:
а) в список L новый элемент X перед всеми вхождениями элемента Y, если Y входит в L;
б) в непустой список L пару новых элементов X1 и Х2 перед его последним элементом;
13
в) в непустой список L, элементы которого упорядочены по возрастанию, новый элемент X так, чтобы сохранилась упорядоченность.
3. Написать программу, которая удаляет:
а) изспискаL закаждымвхождениемэлементаX одинэлемент, если такой есть и он отличен от X;
б) из списка L один элемент, предшествующий элементу X, если такой есть и он отличен от X.
4. N человек расположились по кругу. Начиная с некоторой позиции мы считаем по кругу и каждый М человек выбывает из круга, при этом кругсмыкается. Написатьпрограмму, котораяопределяетпорядок, вкотором люди выбывают из круга.
5a. Определить отношение ПЕРЕВОД (Список1, Список2)
для перевода списка чисел от 0 до 9 и список соответствующих слов. Например,
ПЕРЕВОД ([3, 5, 2, 3], [три, пять, два, три ]). Используйте следующее отношения:
ОЗНАЧАЕТ (0, нуль). ОЗНАЧАЕТ (1, один). ОЗНАЧАЕТ (2, два).
…
5б. Написать программу для отношения ДВОЙНОЙ_СПИСОК (Список1, Список2), в котором каждый элемент Списка1 удваивается и записывается в Список2, например, ДВОЙНОЙ_СПИСОК ([1, 2, 3], [1 ,1 ,2 ,2 ,3 ,3]).
6a. Написать программу для предиката ПОДСТАНОВКА (X,Y, Список1, Список2),
где Список2 – результат подстановки Y вместо всех вхождений X в Список1. Например, отношение ПОДСТАНОВКА (а, х, [а, b, а, с], [х, b, х, с]) – истинно, а отношение ПОДСТАНОВКА (а, х, [а, в, а, с], [ а, в, х, с]) – ложно.
6б. Написать программу для отношения Без_Повторений (Список1, Список2),
14
где Список2 – результат удаления всех повторных вхождений элементоввСписок1, например, Без_Повторений([а, в, с, в],[а, в, с]) – истинно.
7. Написать программу, которая:
а) переносит в конец непустого списка L его первый элемент;
б) переносит в начало непустого списка L его последний элемент; в) проверяет, есть ли в списке L хотя бы два одинаковых элемента; г) вставляет в список L за первым вхождением элемента X все эле-
менты списка L1, если X входит в L;
д) в списке L из каждой группы подряд идущих равных элементов оставляет только один;
е) подсчитываетчисловхожденийкаждогоэлементаспискаL иформирует новый список L1, в котором каждый элемент – это список, состоящий из элемента и числа его вхождений в список L. Например, если
L=[f, d, f, g, a, f, d], то L1=[[f, 3], [d, 2], [g, 1], [a, 1]]. 8а. Написать программу для отношений
ЧЕТНАЯ_ПЕРЕСТАНОВКА (Список1,Список2)
и НЕЧЕТНАЯ_ПЕРЕСТАНОВКА (Список1, Список2),
где Список2 – это переставленный Список1 за четное или за нечетное число шагов. Например,
ЧЕТНАЯ_ПЕРЕСТАНОВКА ([1,2,3],[2,3,1]) НЕЧЕТНАЯ_ПЕРЕСТАНОВКА ([1,2,3],[2,1,3])
8б. Определить два предиката ЧЕТНАЯ_ДЛИНА(Список) иНЕЧЕТНАЯ_ДЛИНА(Список) таким
образом, чтобы они были истинными, если их аргументом являетсясписок четной или нечетной длины соответственно.
Практическая работа
Содержание работы
1.Изучить теоретический материал подраздела 5.1 пособия.
2.Разработать и отладить программу решения индивидуального задания.
15
3.Продемонстрировать преподавателю работу составленных программ.
4.Оформить отчет по выполненной работе.
5.Защитить работу.
5.2.ПРЕДИКАТ ОТСЕЧЕНИЯ
ВПрологе имеется единственный системный предикат – отсечение, влияющий на процедурное поведение программы. В тексте этот предикат обозначается символом «!». Основное назначение этого предиката состоит в ограничении пространства поиска при вычислениях за счет динамического сокращения дерева поиска.
Смысл отсечения можно рассмотреть на предложении общего вида:
Y : – D1, D2, , Dk , !,Dk 2, , Dm.
Будем считать, что предложение активизировалось, когда некоторая цель G сопоставилась с Y и цели D1, , Dk выполнились. Выполнение отсечения приведет к следующему результату.
1.Текущее решение D1, , Dk фиксируется, и все возможные оставшиеся альтернативы больше не рассматриваются.
2.Если некоторое Di при i > k не выполняется, то возврат происхо-
дит не далее чем к моменту отсечения.
3.Отсечение не влияет на цели, расположенные правее его.
4.Если процесс возврата фактически привел к предикату отсечения, то отсечение становится невыполненным и процесс поиска возвращается к последнему выбору, сделанному перед сопоставлением цели G
спредложением Y.
Отсечение считается «зеленым», если оно не влияет на значение программы, т. е. множество решений, получаемых в программе без отсечений, совпадает с множеством решений, получаемых в этой же программе сдобавлениемпредиката«!». «Зеленые» отсечениялишьотбрасываютте пути вычисления, которые не приводят к новым решениям.
Отсечения, не являющиеся «зелеными», называются «красными». Существуют ограничения в применении отсечения: оно может нарушить соответствие между декларативным и процедурным смыслом программы. Поэтому хороший стиль программирования предполагает осторожное применение отсечений и отказ от них без достаточ-
ных оснований.
16
В качестве примера использования предиката отсечения можно рассмотреть функцию поиска максимума из трех чисел:
max3(A, B, C, Max):-
A > B, A > C, !, Max = A; B > C, !, Max = B. max3(_A, _B, C, Max):- Max = C.
Нарис. 5.2 показано дерево поискарешениядляприведенного выше примера.
Рис. 5.2
ЕСЛИ A > B И A > C, ТО Max = A
ИНАЧЕ (ЕСЛИ A =< B ИЛИ А =< C) ЕСЛИ В > С, ТО Мах = В ИНАЧЕ(ЕСЛИОБАУСЛОВИЯВЫШЕНЕВЫПОЛНИЛИСЬ) Мах= С
Из примера видно, что отсечение можно понимать как логическое следствие, ограничивающее поиск решений, если одно решение уже найдено.
В данном случае переменные А, В и С должны быть конкретизированы, так как иначе возникнет ошибка при выполнении оператора сравнения (нельзя сравнивать неинициализированные переменные).
Вариантответавозможентолькоодин, посколькувкаждойветвиалгоритма, кроме последней, стоят отсечения. Убрать отсечения нельзя, так как это приведет к некорректной работе (например, даже если А – наибольшеечисло, будутпроверятьсяB > C иеслиусловиевыполнится, то программа вернет в качестве ответа и A, и B {по очереди}).
17
5.3. ДЕРЕВЬЯ
Несмотря на то что с помощью списков можно представить любую структуру данных, использование их не всегда эффективно. В некоторых случаях при разработке эффективных программ более подходящим типомданныхбудутразличныедревовидныеструктуры. Вэтомподразделе рассматриваются двоичные деревья.
Двоичное дерево либо пусто, либо состоит из корня, левого поддерева, правого поддерева. Корень может быть чем угодно, а поддеревья сами должны быть двоичными деревьями. На рис. 5.3 показано представление арифметического выражения a + bc двоичным деревом.
Рис. 5.3
Существует много способов представления двоичных деревьев на Прологе. Одна из простых возможностей – описывать каждую вершину дерева как соответствующий функтор, аргументами которого являются поддеревья. Тогда дерево (рис. 5.3) будет представлено так:
plus(a, mult(b, c))
В Turbo-Прологе данной структуре должно соответствовать следующее описание в области domains:
domains
exe = plus(exe, exe); mult(exe, exe); symbol
Обратите внимание, что рекурсивной структуре соответствует и рекурсивноеописание. Такоеописаниепозволяетпостроитьдереволюбой сложности (выражение любой сложности), которое включает только
18
операции умножения и сложения. Для того чтобы задать традиционное выражение, необходимо изменить описание домена – ехе, введя в это описаниеновыефункторы, соответствующиерасширенномумножеству операций. Такое представление имеет недостаток, заключающийся в том, что для каждой вершины дерева нужен свой функтор. Это невозможно, если вершины сами являются структурными объектами.
Более универсальный способ представления двоичных деревьев состоит в том, чтобы определить функтор с тремя аргументами (корня и двух поддеревьев) и ввести символ для обозначения пустого дерева. Если пустое дерево представим атомом nil, а в качестве функтора примем tree (L, X, R), где X – корень дерева, L – левое поддерево, R – правое поддерево, то дерево (рис. 5.3) будет выглядеть следующим образом:
tree (+, tree (a, nil, nil), tree (*, tree( b, nil, nil), tree(c, nil, nil))).
Данной структуре будет соответствовать следующее описание:
domains
exe = tree (symbol, exe, exe); nil
Структура логической программы, обрабатывающей дерево, соответствует рекурсивной природе таких данных, как деревья, арифметические выражения и пр. Как правило, эта программа содержит предло- жение-факт для пустого дерева или факт свершившегося действия и одно или несколько рекурсивных предложений, обрабатывающих левое и правое поддерево. Например, ниже приведена программа проверки принадлежности элемента бинарному дереву. Элемент является первым аргументом отношения tree_member, дерево – вторым.
tree_member(X, tree(X, Left, Right)). % факт, устанавливающий
% совпадение элемента с вершиной дерева tree_member(X, tree(Y, Left, Right)): – tree_member(X, Left). tree_member(X, tree(Y, Left, Right)): – tree_member(X, Right).
%рекурсивные предложения, осуществляющие проверку
%принадлежности элемента левому и правому поддереву
Типичные операции над деревьями:
проверка принадлежности дереву;
сравнение деревьев;
19
обходы бинарного дерева (сверху вниз, слева направо, снизу вверх).
Рассмотрим несколько примеров программ работы с деревьями. 1. Программа, позволяющая определить бинарное дерево из элемен-
тов множества, вводимых с клавиатуры (рис. 5.4). Domains
x = integer
y = tree(x, y, y); nil predicates bintree(у)
clauses
bintree(tree(X, L, R)):-readint(X), X<>0, bintree(L), bintree(R), !. бин_дерево(nil):-!.
Goal: bintree(Х).
Рис. 5.4
2. Поиск элемента в упорядоченном дереве.
Упорядоченнымдеревомназываетсятакое, вкоторомэлементправого поддерева больше корня, а элемент левого поддерева меньшекорня.
printree(X, tree(X, L, R)):-!.
printree (X, tree(Y, L, R)):-X>Y, printree (X, R),!. printree (X, tree(Y, L, _)):- printree (X, L).
3. Вывод дерева слева направо В ряде случаев требуется отображать структуру дерева. Мы будем
«рисовать» дерево слева направо, т. е. слева у нас будет корень, а справа – поддеревья. При выводе узла его нужно «сдвинуть» вправо в соответствии с номером уровня, на котором он расположен в дереве.
20
