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

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

.pdf
Скачиваний:
0
Добавлен:
08.09.2026
Размер:
2 Мб
Скачать
☆
предварительно сравнив искомый элемент с корнем. Алгоритм поиска в двоичном дереве будет иметь следующий вид:
если элемент совпадает с корнем дерева, то элемент найден; если искомый элемент меньше корня, то следует искать в левом
поддереве; если искомый элемент больше корня, то следует искать в правом поддереве;
если справочник пуст, то поиск не удался. member_tree(X,tree(X,_,_)).
member_tree(X,tree(Y,Left,_)):-Y>X,!,
member_tree(X,Left).
member_tree(X,tree(_,_,Right)):-member_tree(X,Right).
Поиск элемента в двоичном справочнике эффективнее поиска в списке. Для списка в среднем будет просмотрена примерно половина элементов. Для справочника время поиска пропорционально глубине дерева – длине самого длинного пути между корнем и листом дерева. Для хорошо сбалансированных деревьев (левое и правое поддеревья должны содержать примерно одинаковое количество элементов) глубина дерева пропорциональна log n, где n – количество элементов в справочнике. Если происходит разбалансировка дерева, то фактически дерево превращается в список и его глубина становится близкой к n.
2.13.1.1 Отображение деревьев
Если вывести дерево на экран предикатом write, то дерево выведется в виде прологовского терма, который сложно читать. Поэтому лучше написать предикат вывода дерева в специальной форме, чтобы была видна его структура. Проще всего отображать дерево в повернутой на 90форме и отображать его не сверху вниз, а слева направо. Например, дерево из примера 1 п.2.13.1 отобразится в виде:
Рисунок 19 – Отображение двоичного дерева
91
Определим предикат show_tree(tree) для отображения на экране дерева слева направо. Опишем работу предиката:
отображает правое поддерево с отступом вправо; выводит корень дерева; отображает левое поддерево с отступом вправо.
Для вывода дерева с заданным отступом определим предикат
show_tree(tree, space), а для печати нужного количества пробелов – предикат print_blank(space).
domains
tree=tree(root,tree,tree); nil root=integer space=integer
predicates
show_tree(tree) show_tree(tree,space) print_blank(space)
clauses
show_tree(Tree):-show_tree(Tree,0). show_tree(nil,_). show_tree(tree(X,Left,Right),Space):­ Space1=Space+4, show_tree(Right,Space1), print_blank(Space), write(X),nl, show_tree(Left,Space1). print_blank(0). print_blank(Space):­ write(“ “), Space1=Space-1, print_blank(Space1).
Попробуйте написать предикат, печатающий дерево сверху вниз.
2.13.1.2 Обходы деревьев
Во многих приложениях, использующих бинарные деревья, требуется
доступ к элементам дерева. Основой этого доступа является обход дерева в предписанном порядке. Имеются три возможности линейного упорядочивания при обходе: сверху вниз (сначала корень дерева, затем вершины левого поддерева, после этого вершины правого поддерева); слева направо (сначала вершины левого поддерева, затем корень, после этого вершины правого поддерева); снизу вверх (сначала вершины левого поддерева, затем вершины правого поддерева, после этого корень).
92
Пример 1: Для дерева из примера1 обход сверху вниз даст следующую
последовательность вершин: 5, 4, 8, 6, 2, 3, 1, 7; обход слева направо даст: 6, 8, 2, 4, 3, 5, 7, 1; обход снизу вверх даст: 6, 2, 8, 3, 4, 7, 1,5.
Опишем предикат uptodownround(tree,list), формирующий список из
вершин, полученных при обходе дерева сверху вниз. Для соединения обходов поддеревьев и корня в обход дерева будем использовать предикат
append(list,list,list), который соединяет два списка в третий.
uptodownround(tree(X,Left,Right),S):­ uptodownround(Left,Ls), uptodownround(Right,Rs), append([X|Ls],Rs,S). append([],S,S). append([Head|Tail],S,[Head|Tail1]):­ append(Tail,S,Tail1).
Очевидно, что все обходы будут отличаться только порядком соединения
обходов поддеревьев и корня.
Для обхода слева направо соединение обходов будет иметь вид:
append(Ls,[X|Rs],S).
А для обхода сверху вниз получим:
append(Rs,[X],Rs1), append(Ls,Rs1,S).
Перечисленные обходы используются для поиска минимального или максимального элемента в дереве, преобразовании дерева.
2.13.2 Графы
Во многих приложениях для представления отношений, ситуаций, структур задач используют графы. Граф определяется множеством вершин и ребер. Каждое ребро соединяет две вершины. Если ребра направленные, то их называют дугами. Графы с ребрами – дугами называются направленными. Ребрам могут быть приписаны стоимости, тогда граф называется взвешенным. В Прологе граф можно представить в виде отдельных предложений об имеющихся ребрах или описать граф как список ребер и список вершин, объединенных функтором graph (в случае если каждая вершина соединена хотя бы с оной вершиной, список вершин можно опустить, т.к. он неявно содержится в списке ребер).
Пример 1: Рассмотрим различные способы описания графа, представленного следующим рисунком:
Рисунок 20 – Граф из примера 1
93
В первом случае имеем описание:
edge(a,b). edge(a,d). edge(a,e). edge(b,e). edge(c,d). edge(c,e). edge(d,e).
Во втором случае граф опишется как структура:
domains vertex=symbol edge= edge(vertex,vertex) list_vertex=vertex* list_edge=edge* graph= graph(list_vertex,list_edge)
Тогда граф из примера 1 можно описать как
G=graph([a,b,c,d,e],[edge(a,b), edge(a,d), edge(a,e), edge(b,e), edge(c,d), edge(c,e), edge(d,e)]).
Способ представления графа зависит от конкретной задачи и от выполняемых операций над графами.
Пример 2: Определим предикат path(A,Z,Path), который будет находить ациклический путь Path (список вершин, в котором каждая вершина присутствует только один раз) из вершины A в вершину Z. Для графа из примера 1 верны следующие предикаты:
path(a,c,[a,b,c]) path(a,c,[a,b,e,c]) path(a,c,[a,d,c]) path(a,c,[a,d,e,c]) path(a,c,[a,d,e,b,c]) path(a,c,[a,e,d,c]) path(a,c,[a,e,b,c])
Опишем предикат path:
если вершины A и Z совпадают, то список Path состоит из одной вершины; в противном случае находим ациклический путь Path1 из какой-нибудь вершины Y в Z, а затем ациклический путь из A в Y, не содержащий вершин из Path1.
Рисунок 21 – Поиск ациклического пути
94
domains
vertex=symbol path=vertex*
predicates
member(vertex,path) neighbour(vertex,vertex) edge(vertex,vertex) path(vertex,vertex,path) path1(vertex,path,path) find_path(vertex,vertex) edge(vertex,vertex)
goal
write("Введите начальную вершину\n"), readln(A), write("Введите конечную вершину\n"), readln(B), find_path(A,B).
clauses
edge(a,b). edge(a,d). edge(a,e). edge(b,e). edge(c,d). edge(c,e). edge(e,d). edge(d,e). find_path(A,B):-path(A,B,Path),!, write("Путь=",Path). find_path(_,_):-write("Пути нет"). path(A,Z,Path):-path1(A,[Z],Path). path1(A,[A|Path1],[A|Path1]). path1(A,[Y|Path1],Path):­ neighbour(X,Y), not(member(X,Path1)), path1(A,[X,Y|Path1],Path). member(X,[X|_]). member(X,[_|Tail]):-member(X,Tail). neighbour(X,Y):-edge(X,Y); edge(Y,X).
Заметим, что данная программа находит первый попавшийся путь из вершины A в B. Предикат path можно использовать для решения разнообразных задач. Например, для поиска всех вершин, достижимых из заданной, для поиска пути заданной длины от выделенной вершины, для нахождения диаметра графа (максимального расстояния между двумя
95
вершинами), гамильтонова цикла (цикла, проходящего через все вершины), количества компонент связности графа. При решении этих задач удобно использовать для обмена значениями между предикатами динамическую базу данных.
Пример 3: Модифицируем приведенную в примере 2 программу для поиска пути минимальной стоимости между двумя вершинами во взвешенном графе.
domains
vertex=symbol path=vertex* length_path=integer weigth=integer
database
bd_path(path,length_path)
predicates
member(vertex,path) neighbour(vertex,vertex, weigth) edge(vertex,vertex,weigth) path(vertex,vertex,path,length_path) path1(vertex,path, length_path,path,length_path) find_begining(vertex,vertex) find_path(vertex,vertex) edge(vertex,vertex)
goal
write("Введите начальную вершину\n"), readln(A), write("Введите конечную вершину\n"), readln(B), find_begining(A,B), find_path(A,B), bd_path(Path,Length), write("Путь=",Path,”Длина=”,Length), retractall(_).
clauses
find_begining(A,Z):-
path(A,Z,Path,Length), assertz(bd_path(Path,Length)). path(A,Z,Path,Length):-path1(A,[Z],0,Path,Length). path1(A,[A|Path1],Length,[A|Path1], Length). path1(A,[Y|Path1],Length1,Path,Length):­ neighbour(X,Y,Weigth), not(member(X,Path1)), Length2=Length1+1+Weigth; path1(A,[X,Y|Path1],Length2,Path,Length).
96
member(X,[X|_]). member(X,[_|Tail]):-member(X,Tail). neighbour(X,Y):-edge(X,Y); edge(Y,X). find_path(A,B):-
bd_path(Path,Length),
path(A,B,Path1,Length1),
Length1<Length,!,
retract(bd_path(Path,Length)),
assertz(bd_path(Path1,Length1)),
find_path(A,B). find_path(_,_).
2.14 Стратегии решения задач
Пролог использует неэффективный метод поиска – перебор. Можно
написать свои программы поиска решений. В этом случае описания задач будем представлять в виде пространства состояний – ориентированного графа, вершины которого соответствуют так называемым проблемным ситуациям, которые встречаются в задаче, дуги графа соответствуют разрешенным переходам из одних состояний в другие. Конкретная задача определяется начальной и целевой ситуациями. Тогда поиск решения сводится к поиску пути в графе из стартовой вершины в целевую вершину. При поиске пути возникает проблема обработки альтернативных путей поиска. Кроме того, каждому переходу из одного состояния в другое можно приписать его стоимость. В этом случае мы заинтересованы в отыскании пути минимальной стоимости. При этом стоимость пути равна сумме стоимостей дуг, из которых состоит этот путь. Даже в случае, если стоимости на заданы, нас может интересовать кратчайший путь. Существуют три основные стратегии перебора альтернатив: поиск в глубину, поиск в ширину и поиск с предпочтением.
2.14.1 Стратегия поиска в глубину
Стратегию поиска в глубину уже была рассмотрена в предикате path из
примера 3 п.2.13.2. Поиск в глубину аналогичен поиску самой Пролог-системы, для выбора альтернативы для поиска выбирается вершина, расположенная дальше других от стартовой. Но для бесконечных пространств состояний стратегия поиска в глубину может не привести к решению. В этом случае можно ввести ограничение на глубину поиска.
path_depth(A,Z,Path,Length):-path1_depth(A,[Z],Path, Length). path1_depth(A,[A|Path1],[A|Path1],_). path1_depth(A,[Y|Path1],Path, Length):­ Length>0, edge(X,Y),
97
not(member(X,Path1)), Length1= Length-1 path1_depth(A,[X,Y|Path1],Path, Length).
2.14.2 Стратегия поиска в ширину
При поиске в ширину в первую очередь осуществляется переход в
вершины, наиболее близкие к стартовой. Реализация такого поиска является более сложной задачей, т.к. необходимо сохранять все множество путей­кандидатов. Каждый путь-кандидат будет представлять список вершин в обратном порядке (т.е. стартовая вершина будет последним элементом, а последняя порожденная вершина будет головой списка). Все пути-кандидаты будут храниться в списке. Первоначально список путей-кандидатов содержит один элемент [A], где A – стартовая вершина. Опишем поиск в ширину: если голова первого пути – это целевая вершина, то этот путь является решением; в противном случае, удаляем первый путь из множества путей­кандидатов, порождаем множество всех возможных продолжений этого пути на один шаг, добавляем множество продолжений в конец множества-кандидатов и продолжаем поиск в ширину для нового множества- кандидатов.
path_breadth(A,Z,Path):-path1_breadth(Z,[[A]],Path). path1_breadth(Z,[[Z|Path1]|_],[Z|Path1]). path1_breadth(Z,[[Y|Path1]|Path2],Path):­ findall([Y1,Y|Path1],continue(Y,Y1,Path1),New_path), append(Path2,New_path,Path3),!, path1_breadth(Z,Path3,Path). path1_breadth(Z,[_|Path2],Path):- path1_breadth(Z,Path2,Path). continue(X,Y,Path):- edge(X,Y), not(member(Y,Path)).
Стратегия поиска в ширину гарантирует получение кратчайшего пути
первым, что не всегда верно в случае поиска в глубину. Однако, для взвешенного графа поиска в ширину будет недостаточно, в этом случае используют поиск с предпочтением.
В случае больших пространств состояний существует опасность
комбинаторного взрыва, т.к. с увеличением длин путей наблюдается экспоненциальный рост объема множества путей-кандидатов. В этом случае для управления поиском решений используют эвристики – информацию для управления поиском решений в конкретной предметной области.
98
Расчетно-графическое задание
(Каждый вариант состоит из четырёх задач и выполняется на Прологе).
1. Операции со списками.
1.1 Исходный список преобразуйте в новый "двойной" список таким
образом, чтобы каждый элемент исходного списка удвоился.
Например: [1,2,3]->[1,1,2,2,3,3].
1.2 Удалите из списка все повторные вхождения элементов.
Например: [1,2,1,4,1,2,3]->[1,2,4,3].
1.3 Сформируйте новый список из общих элементов двух списков
(пересечение множеств).
Например: [1,2,3,5],[6,4,1,8,3]->[1,3].
1.4 Сформируйте новый список, включающий в себя без повторений все
элементы двух списков (объединение множеств).
Например: [1,2,3,4], [6,3,1,4]->[1,2,3,4,6].
1.5 Сформируйте новый список из тех элементов первого списка, которые
не входят во второй (разность множеств).
Например: [1,2,3,5],[6,3,1,8]->[2,5].
1.6 Сформируйте новый список из всех возможных произведений
элементов двух списков (декартово произведение множеств).
Например: [1,2,3],[2,4]->[2,4,8,6,12].
1.7 Осуществите циклический сдвиг в списке на один элемент вправо.
1.8 Осуществите циклический сдвиг в списке на один элемент влево.
1.9 Подсчитайте число вхождений каждого элемента списка и
сформируйте новый список, каждый элемент которого - это список из двух элементов: самого элемента и числа его вхождений в список.
Например: [2,4,2,3,2,3]->[[2,3],[4,1],[3,2]].
1.10 По кругу расположены n человек. Начиная с некоторой позиции
считаем от 1 до m по кругу. Каждый m-ый человек выбывает из круга, при этом круг смыкается. Определите порядок выбывания из круга.
1.11 В непустой список, упорядоченный по неубыванию, добавьте новый
элемент таким образом, чтобы сохранилась упорядоченность.
1.12 Список целых чисел разделите на два списка: из чётных элементов и
нечётных элементов.
Например:[1,-2,3,5,-4]->[1,3,5],[-2,-4].
1.13 Преобразуйте список таким образом, чтобы все вхождения X в
исходный список заменились на Y.
Например:[1,2,1,4,2,1],X=1,Y=0->[0,2,0,4,2,0].
99
1.14 В списке слов найти слово максимальной длины и поставить его
последним.
1.15 В списке слов найти слово минимальной длины и поставить его
первым.
1.16 Проверить является ли слово палиндромом, т.е. одинаково ли оно
читается слева направо и наоборот.
1.17 Найдите элемент списка по его номеру.
1.18 Удалите из списка все вхождения элемента X.
Например: [1,2,1,3,1,1,2,5,6],X=1->[2,3,2,5,6].
1.19 Возведите в квадрат каждый третий элемент списка.
1.20 В списке из каждой группы подряд идущих элементов оставьте
только один.
Например: [1,1,2,2,3,3,3,4,4,4,4]->[1,2,3,4].
1.21 Вставьте в список новый элемент X перед всеми вхождениями Y,
если Y входит в исходный список.
Например: [1,2,1,5,8,1,0],X=5,Y=1->[5,1,2,5,1,5,8,5,1,0].
1.22 Удалите из списка перед каждым вхождением X один элемент, если
такой имеется и отличен от X.
Например: [1,2,1,1,3],X=1->[1,1,1,3].
1.23 Удалите из списка за каждым вхождением X один элемент, если
такой имеется и отличен от X.
Например: [1,8,2,1,1,5,1],X=1->[1,2,1,1,1].
1.24 Сформируйте новый список без повторений из тех элементов
исходного списка, которые встречаются больше одного раза.
Например:[5,1,2,1,3,5,5,7]->[5,1].
1.25 Вставьте в список элементы другого списка после каждого
вхождения X, если X входит в исходный список.
Например: [1,2,1,5,3],X=1,[0,7,9]->[1,0,7,9,2,1,0,7,9,5,3].
2. Строки, файлы.
2.1 Текстовый файл состоит из нескольких строк, длина каждой из
которых меньше 40. Выровняйте строки текста по ширине (равномерным внесением дополнительных пробелов между словами).
2.2 Запишите в новый файл все строки исходного файла, содержащие в
качестве фрагмента заданную строку.
2.3 Определите, совпадают ли два текстовых файла. Если нет, то
получите номер первой строки и позиции первого символа в этой строке, в котором файлы отличаются между собой.
100
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]