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

Точно Не проект 2 / Не books / Источник_1

.pdf
Скачиваний:
10
Добавлен:
01.02.2024
Размер:
20.67 Mб
Скачать

340

Глава 6

 

 

что приводит к отсечению всех ветвей вычислений, порождаемых преди-

катом repeat.

Многие версии языка Пролог имеют и другие встроенные предикаты ввода-вывода, учитывающие особенности конкретных внешних устройств. Программировать сложный ввод-вывод с помощью таких предикатов проще, чем на основе стандартных предикатов read и write.

6.10.2. Предикаты обработки термов

Рассмотрим некоторые встроенные предикаты Пролога, выполняющие обработку термов. Такие предикаты обеспечивают проверку типа значений терма, выясняют, была ли некоторая переменная конкретизирована, выделяют составные части терма, образуют новые термы и т. п.

Выяснить тип терма можно с помощью следующих встроенных предикатов, которые имеются во многих реализациях языка Пролог:

var(X) – возвращает значение значение истина, если Х – не конкретизированная переменная;

nonvar(X) – предикат nonvar будет истинным, если Х – терм любого вида, кроме не конкретизированной переменной;

atom(X) – предикат atom будет истинным, если Х – атом;

atomic(X) – предикат atomic даст значение истина, если Х обозначает целое число или атом;

integer(X) – предикат integer будет истинным, если Х обозначает целое число.

Следующие примеры иллюстрируют применение некоторых предикатов:

?– var(X). Yes

?– X=пролог, var(X). No

?– X= пролог, integer(X). No

?– atom(пролог). Yes

?– atom(220). No

?– atomic(220). Yes

ВПрологе имеется группа встроенных предикатов, которые обеспечивают декомпозицию термов на составные части и конструирование термов из простых элементов. К ним относятся следующие предикаты:

name(A,L), functor(T,F,A), arg(N,T,A), T = ..L. Рассмотрим их.

Язык Пролог

341

 

 

Вызов предиката name вернёт значение истина, если L – список ASCII-кодов символов, образующих атом А, например:

?– name (X, [97,105]). X = ai

?– name (ab,L).

L = [97,98]

Предикат functor(T,F,A) будет истинным, если главный функтор F имеет терм Т с арностью А, например:

? – functor(f(a, x), F, A). F = f, A = z

Вызов предиката arg(N,T,A) вернёт значение истина, если аргумент А

N-ый аргумент терма Т, например:

?– arg(2, f(x, a (1), b), A). A = a(1)

Предикат T = ..L будет истинным, если L– список, первым элементом которого является главный функтор терма Т, за которым следуют аргументы терма Т, например:

?– f(a, b) = ..L. L = [f, a, b]

?– T = ..[быстро, автомобиль]. T = быстро(автомобиль).

Приведем более содержательные примеры использования рассмотренных предикатов при создании и декомпозиции термов.

Пример 1. Определим предикат ввод_атома, выполняющий чтение символов из входного потока и формирующий из них атом. При этом воспользуемся предикатом name. Определяемый предикат будет считывать символы входного потока, пока не встретится символ начала новой строки. Аргументом предиката ввод_атома(Х) является переменная, которая конкретизируется последовательностью символов, вводимых пользователем.

ввод_атома(Х):–

% ввести символ

get0(C),

% если С- символ перехода

(С=13;

% на новую строку, то

 

% закончить работу

ввод_символов(С, Cписок)),

% ввести символы в список

name(X, Cписок)),!.

% преобразовать список в атом

342

Глава 6

 

 

% ввод символов в список, пока не будет нажата клавиша Enter

ввод_символов(13, [ ])):–!.

ввод_символов(С, [C T]):– get0(C1), ввод_символов(С1, Т),!.

Здесь дополнительно определен предикат ввод_символов. Данный предикат определен рекурсивно. На каждом шаге рекурсии очередной символ из входного потока вставляется в начало формируемого списка. Список формируется в обратном порядке, т. е. его заполнение начинается с обнаружения ситуации нажатия клавиши Enter (символ с ASCII кодом 13). После этого в пустой список добавляются все символы, которые были введены ранее. В частности, при вызове предиката ввод_атома(Х) переменная Х получит следующее значение:

? – ввод_атома(Х).

Иванов_Петр % ввод с клавиатуры Х = ‘Иванов_Петр’

Пример 2. Сформируем программно с помощью предиката functor факт data и запомним его в переменной D.

?–functor(D, data, 3), arg(1, D, 28), arg(2, D, июль), arg(3, D, 2001).

D = data(28, июль, 2001).

Пример 3. Определим предикат map, являющийся аналогом функционала map языка Лисп. Предикат map будет иметь три аргумента. Первый аргумент будет являться функтором, два других аргумента будут представлены списками. Предикат map будет обеспечивать применение функтора последовательно к первым элементам указанных списков, ко вторым элементам и т. д.

map(_, [ ], [ ]).

map(F, [X1 Xt], [Y1 Yt]): –

Q=..[F,X1,Y1], call(Q), %применение терма F(X1,Y1) map(F, Xt, Yt).

Пусть

square(X, Y): – Y is X*X.

Тогда при вводе запроса

? – map(square,[1, 2], Y).

Язык Пролог

343

 

 

будет получен ответ

Y = [1, 4].

6.10.3.Предикаты работы с базой данных

ВПрологе имеется ряд встроенных предикатов, которые позволяют корректировать базу данных в процессе выполнения программ. К ним от-

носят следующие предикаты: assert(X), asserta(X), assertz(X), retract(X).

Предикат assert(X) добавляет в базу данных утверждение Х. Напри-

мер:

?– assert(столица(‘Украина’, ‘Киев’)). Yes

?– столица(‘Украина’, X).

X = Киев

Предикат asserta(X) добавляет утверждение Х в базу данных и помещает его перед другими утверждениями, соответствующими этому же факту или правилу. Предикат assertz(X) определяется аналогично asserta(X), но утверждения помещаются после других утверждений. Предикат retract(X) выполняет поиск утверждения Х в базе данных и удаляет первое найденное утверждение.

Совместное использование рассмотренных предикатов позволяет изменять базу данных. Определим простой предикат, для изменения значения факта, указывающего текущий день месяца:

день (9). изменить_день:-

день(Х), Y is X+1, retract(день (Х)), assert(день (Y).

При каждом вызове предиката изменить_день текущая дата будет увеличиваться на единицу:

?– изменить_день. Yes

?– день(Х).

Х= 10.

Если предикаты группы assert используются для добавления в базу данных правила, то добавляемое правило дополнительно заключается в скобки:

? – assert((a(X): – b(X), с(X))).

344

Глава 6

 

 

Предикат assert позволяет накапливать в базе данных уже вычисленные ответы на вопросы [7]. Например, пусть в программе используется предикат решить(Задача,Решение). Тогда можно задать вопрос и добавить полученное решение в базу данных:

? – решить(Задача1, Решение), asserta(решить(Задача1, Решение)).

Если теперь задавать вопросы, использующие добавленный факт решить(Задача1, Решение), то ответ будет получен быстрее, так как никаких вычислений выполнять уже не требуется. Пролог будет извлекать в качестве ответов запомненные факты.

Во многих реализациях Пролога имеется встроенный предикат retractall(X), который удаляет из базы данных все утверждения, функтор и арность которых сопоставимы с Х. Данный предикат может быть определен следующим образом:

retractall(Факт): – retract(Факт), fail. retractall(Голова): – retract((Голова: – Tело)),

fail.

retractall(_).

Пользователь может вводить новые утверждения в базу данных из файла, используя предикат consult(Файл). Например, запрос

? – consult(‘prog1.аri’).

считывает содержимое файла prog1.ari”. Если при чтении утверждений из файла обнаруживается синтаксическая ошибка, то выдается соответствующее сообщение, но обработка оставшихся утверждений продолжается.

Утверждения, считываемые предикатом consult(Файл) добавляются в конец существующей базы данных. Допускается замена существующих утверждений базы данных новыми утверждениями из файла. В этом случае используется предикат reconsult(Файл).

После модификации базы данных с помощью рассмотренных выше предикатов, пользователь имеет возможность запомнить текущее состояние системы в файле. Для этого применяется предикат save(Файл). Восстановить запомненное состояние системы можно, воспользовавшись предикатом restore(Файл).

Для просмотра утверждений, входящих в базу данных, можно воспользоваться предикатами:

listing – выводит утверждения, содержащиеся в базе данных, в выходной поток;

listing(X) – печатаются утверждения, сопоставимые с термом Х.

Язык Пролог

345

 

 

6.11.Решение задач на языке Пролог

6.11.1.Создание и обработка бинарных деревьев

Одной из широко используемых структур данных, применяемых в программировании, являются бинарные деревья. Бинарное дерево – это структура данных, состоящая из левого бинарного поддерева, корня и правого бинарного поддерева. Элементы, расположенные в вершинах бинарного дерева, упорядочены в соответствии с некоторым признаком. При этом любой элемент левого поддерева всегда меньше корневого элемента, а любой элемент правого поддерева всегда больше корня. На рисунке 6.9 изображено бинарное дерево, вершины которого представлены буквами. Дерево упорядочено в соответствии с алфавитом.

Будем представлять бинарное дерево термом вида дерево(Лд,К,Пд), где Лд – левое поддерево, К – корень, а Пд – правое поддерево. Пустое бинарное дерево будем обозначать атомом nil. Тогда бинарное дерево, изображенное на рисунке 6.9, можно описать следующим образом:

дерево(дерево(дерево(nil, a, nil), д, дерево(nil, e, nil)), ж, дерево(nil, к, nil))

Рисунок 6.9 – Бинарное дерево

Применение бинарных деревьев существенно повышает эффективность поиска элементов данных по сравнению со списками.

Определим отношение принадлежит(Х,Д), которое позволяет установить, входит ли элемент Х в бинарное дерево Д. Определение отношения будет основано на следующих правилах:

а) если Х совпадает с корнем дерева Д, то Х входит в дерево; б) если Х меньше, чем корень дерева Д, то выполнить поиск Х в левом

поддереве; в) если Х больше, чем корень дерева Д, то выполнить поиск Х в пра-

вом поддереве.

Запишем соответствующие утверждения на Прологе:

принадлежит(Х, дерево(Лд, Х, Пд)).

% определение а)

346

Глава 6

 

 

принадлежит(Х, дерево(Лд, К, Пд)): –

% определение б)

больше(К, Х),

 

принадлежит(Х, Лд).

 

принадлежит(Х, дерево(Лд, К, Пд)): –

% определение в)

больше(Х, К)

 

принадлежит(Х, Лд).

 

Предикат больше(Х,Y) обеспечивает сравнение элементов, запоминаемых в узлах дерева, и требует определения при решении задачи поиска в конкретном дереве. Если элементы, находящиеся в вершинах дерева, — это числа, то данное отношение запишется в виде X>Y.

Важно отметить, что эффективность поиска в бинарном дереве зависит от сбалансированности дерева. Бинарное дерево считается приближенно сбалансированным, если для каждой вершины дерева два ее поддерева содержат примерно равное число элементов. Так, если сбалансированное бинарное дерево содержит n=2048 чисел, то при ответе на вопрос

? – принадлежит(27, Д).

Пролог выполнит не более log2n+1 сравнений, т.е. 12. При поиске в линейном списке максимальное число сравнений будет 2048. Поэтому важно не допускать вырождения бинарного дерева в список.

Задача создания бинарного дерева Д1 путем добавления элемента Х к другому бинарному дереву Д может быть решена с помощью предиката добавить(Д, Х, Д1). Правила добавления элементов в дерево следующие:

а) добавление Х к nil дает дерево(nil, X, nil);

б) добавление Х в дерево Д с корнем Х дает Д1=Д; в) если корень дерева больше Х, то Х добавляется в левое поддерево,

если Х больше корня, то Х добавляется в правое поддерево. Соответствующие утверждения запишутся на Прологе следующим

образом:

добавить(nil, X, дерево(nil, X, nil).

добавить(дерево(Лд, Х, Пд), Х, дерево(Лд, Х, Пд)). добавить(дерево(Лд, К, Пд), Х, дерево(Лд1, К, Пд): –

больше(К, Х), добавить(Лд, Х, Лд1). добавить(дерево(Лд, К, Пд), Х, дерево(Лд, К, Пд1): –

больше(Х, К), добавить(Пд, Х, Пд1).

При вводе вопроса

? – добавить(nil, д, Д1), добавить(Д1, в, Д2).

будут получены ответы:

Д1=дерево(nil, д, nil). Д2=дерево(дерево(nil, в, nil), д, nil).

Язык Пролог

347

 

 

Отношение добавить(Д, Х, Д1) можно использовать для преобразования списка в бинарное дерево:

список_в_дерево([ ], nil). список_в_дерево([H|T], Д1): –

список_в_дерево(Т, Д), добавить(Д, Н, Д1).

Рассмотренное отношение не гарантирует построения сбалансированного дерева. При его использовании необходимо следить за тем, чтобы список был неупорядоченным, иначе построенное дерево может оказаться вырожденным. Отношения, обеспечивающие построение приближенно сбалансированных деревьев, рассматриваются в [7].

Иногда требуется выполнить преобразование дерева в список. В этом случае можно воспользоваться определением:

дерево_в_список(nil, [ ]). дерево_в_список(дерево(Лд, К, Пд), С): –

дерево_в_список(Лд, С1), дерево_в_список(Пд, С2),

слияние(С1, [К|C2], C).

Здесь предикат слияние(С1,С2,С3) выполняет слияние списков С1, С2 и формирует список С3. Данный предикат соответствует предикату append, рассмотренному в §4.1.8.

Помимо поиска и добавления, при работе с бинарными деревьями необходимо уметь удалять вершины из бинарного дерева. Если требуется удалить вершину на уровне листа бинарного дерева, то для этого можно воспользоваться отношением добавить:

удалить_лист(Д1,Х,Д): – добавить(Д, Х, Д1).

Иными словами, удаление листа Х из дерева Д1 равносильно поиску такого дерева Д, добавление к которому элемента Х дает дерево Д1.

Если же требуется выполнить удаление элемента на уровне корня дерева, то задача становится сложнее. Основная проблема заключается в том, что удаление корневой вершины приводит к образованию “дыры”. В этом случае левое и правое поддеревья, находящиеся ниже удаленного элемента, могут быть потеряны. Чтобы избежать этого, образовавшуюся дыру необходимо заполнить, сохранив упорядоченность дерева. “Дыру” заполняют либо самым “левым” элементом из правого поддерева, либо самым “правым” элементом из левого поддерева. Ниже приведена программа, реализующая указанные требования:

удалить(дерево(nil, X, Пд), Х, Пд).

% Лд отсутствует

348

Глава 6

 

 

удалить(дерево(Лд, Х, nil), Х, Лд).

% Пд отсутствует

% заполнение "дыры" самым левым элементом Пд удалить(дерево(Лд, Х, Пд), Х, дерево(Лд, У, Пд1)): –

удалить_левый(Пд, У, Пд1).

% поиск удаляемого корня удалить(дерево(Лд, К, Пд), Х, дерево(Лд1, К, Пд)): –

больше(К, Х), удалить(Лд, Х, ЛД1). удалить(дерево(Лд, К, Пд), Х, дерево(Лд, К, Пд1): – больше(Х, К), удалить(Пд, Х, Пд1).

%удаление самого левого элемента из правого поддерева удалить_левый(дерево(nil, У, Пд), У, Пд). удалить_левый(дерево(Лд, К, Пд), У, дерево(Лд1, К, Пд)): –

удалить_левый(Лд, У, Лд1).

6.11.2.Поиск решений в пространстве состояний

Вглаве 2 рассмотрено представление задач в пространстве состояний

иметоды их решения. Пространство состояний – это граф, вершины которого соответствуют состояниям решаемой задачи, а дуги – возможным переходам состояний. При решении конкретной задачи задаются стартовая и целевая вершины. Решению задачи соответствует путь на графе. Рассмотрим программирование основных методов поиска решений в пространстве состояний на языке Пролог.

6.11.2.1. Поиск в глубину. Простейший способ организации поиска в глубину – это использование механизма поиска решений, заложенного в пролог-системе. Рассмотрим граф состояний, изображенный на рисунке

6.10.

Рисунок 6.10 – Граф состояний

Здесь “а” – стартовая вершина, а “g1” – целевая. Данный граф можно описать в виде следующего набора утверждений:

Язык Пролог

349

 

 

b:– a. % "b" достижимо, если достигнуто "a"

c:– a. % "c" достижимо, если достигнуто "а"

d:– b.

. . .

 

g1: – e.

% "g1" достижимо, если достигнуто "е"

а.

% начальное состояние

Для того чтобы узнать, имеет ли рассматриваемая задача решение, необходимо просто задать вопрос:

? – g1.

Получив такой вопрос, пролог-система выполнит поиск в глубину на графе, изображенном на рисунке 6.10. При этом поиск будет выполняться обратным методом – от цели к известным фактам. После обхода всех необходимых вершин графа, будет сформирован ответ “да”.

Для того чтобы осуществить прямой поиск (от фактов к цели), необходимо описать граф состояния (рисунок 6.10) в виде следующего набора правил:

а: – b. b: – d. b: – e.

. . .

e: – g1.

g1. % цель

Вэтом случае процесс поиска решения инициируется вопросом:

?– а.

Преимущество рассмотренного способа программирования поиска в глубину заключается в его простоте. К недостаткам следует отнести следующее:

программа формирует ответ “да” или “нет”, но не возвращает при этом решающий путь, соответствующий ходу решения;

если на графе состояний имеются циклы, то прологсистема может

войти в бесконечный цикл, обусловленный косвенными рекурсивными вызовами.

Чтобы исключить указанные недостатки изменим представление графа состояний. Опишем его не с помощью правил, а с помощью фактов вида:

после(X, Y).

Такой факт означает, что состояние задачи Y непосредственно следует из состояния Х. Тогда граф состояний, изображенный на рисунке 6.10, можно представить следующим набором фактов:

Соседние файлы в папке Не books