Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Логическое программирование. Учебно-методическое пособие к лабораторным работам
.pdf
41
Р а б о т а №5. ИТЕРАЦИОННЫЕ ЛОГИЧЕСКИЕ ПРОГРАММЫ
Цель работы — знакомство с понятием итерационной логической про-
граммы. Изучение различий между итерационными и рекурсивными про-
граммами. Освоение приёмов перехода от рекурсивных к итерационным ло-
гическим программам.
Общие сведения
Рекурсивные логические программы — весьма простой и эффективный
инструмент в решении широкого круга практических задач. Часто структура
рекурсивной программы представляется очевидной уже при грамотном
формулировании поставленной задачи (см. примеры в 4-й работе). Однако
рекурсивные вычислительные процедуры обладают и весьма существен-
ными недостатками. Быстрое заполнение стека и экспоненциальный рост
объёма вычислений — характерные недостатки в практике использования
рекурсии.
При вызове рекурсивной процедуры происходит последовательный
«спуск» от точки вызова к клаузе, означающей окончание рекурсии. При
каждом шаге этого «спуска» в стеке резервируется память под промежуточные значения переменных, используемые в рекурсивном вычислении. По-
сле этого осуществляется обратное движение к точке вызова, при котором
заполняются значениями все зарезервированные области памяти. Такое
возвратно-поступательное движение требует существенных затрат оперативной памяти и вычислительных затрат, объём которых растёт на порядок
быстрее, чем число шагов в аналогичном циклическом вычислении. Однако
возможно преобразовать эти процедуры так, чтобы исключить возвратно-
поступательный ход рекурсивного процесса и преобразовать его в линей-
ный, односторонне направленный.
Такие логические программы называют итерационными. Это название
программы получили в связи со своим фактическим сходством с цикличе-
скими (итерационными) процедурами в императивных языках программиро-
вания (например, в Паскале или Си). В отличие от ранее рассмотренных ре-

42
курсивных программ в итерационных программах требуемая оперативная
память и вычислительные затраты зависят от числа шагов линейно, а не
экспоненциально.
Итерационные программы также являются рекурсивными, но со своими
отличительными признаками. Основным признаком итерационной програм-
мы на практике является праворекурсивный характер рекурсивного правила.
Таким образом предикат рекурсивного вызова должен быть крайним пра-
вым в тексте правила при записи его в одну строку, то есть последним в
списке целей. Таково правило «предок» в последнем примере к 4-й работе.
Являясь праворекурсивным, оно более экономично, нежели его леворекур-
сивный аналог.
В варианте «прямой_предок_по_мужской_линии» оно будет выглядеть
как:
parent (X, Y) :- father(X, Z), parent(Z,Y).
с соответствующей точкой выхода:
parent(X, Y) :- father(X,Y).
Причины преимущества именно таких программ можно понять, если пе-
рейти от декларативного смысла логических программ к их процедурному
смыслу. Так как просмотр клауз при поиске решения ПРОЛОГ-машиной
осуществляется сверху вниз, а просмотр целей внутри каждой клаузы сле-
ва направо, то рекурсивное обращение на последнем шаге поиска решения
может избавить ПРОЛОГ-машину от необходимости запоминать промежуточные значения для возвратного движения рекурсии.
Далеко не всякие рекурсивные программы могут быть так просто пред-
ставлены в праворекурсивном виде, как программа «предок». В этом слу-
чае оказывается необходимым включать в итерационные программы до-
полнительные переменные, называемые накопители.
Благодаря накопителям основные аргументы предикатов не изменяются
внутри клаузы на каждом шаге рекурсии. Находясь в голове и в теле клаузы,
основные аргументы предикатов лишь стоят на фиксированных позициях,
никак иначе не участвуя в вычислениях. Накопители, в соответствии со своим наименованием, «накапливают» как значения счётчиков цикла, так и результирующие значения вычислений.

43
Основные аргументы предикатов в итерационных вычислениях заметны
только в моменты начала и конца итерации. Накопленные значения переходят к основным аргументам от накопителей в точке останова рекурсии (в
точке конца итерации), где имена основных аргументов и соответствующих
им накопителей совпадают. При этом старт рекурсии задаёт конкретные
начальные значения накопителей, в то время как основные аргументы
остаются неозначены.
Старт итерации удобно оформить в виде правила, где голова — предикат только с основными аргументами, а тело клаузы — аналогичный предикат, но имеющий наряду с основными и дополнительные аргументынакопители (см. Пример 5).
Само наличие двух точек: точки начала и точки конца итерации (в отличие от одной точки остановки рекурсии) есть отличительная черта итерационных программ с накопителем, формально отражающая односторонне
направленный вычислительный процесс.
Введение переменных-накопителей в рекурсивные клаузы оказывается
необходимым потому, что значения переменных сохраняются только внутри
клаузы при обращении к ней и не сохраняются при переходе к другой клаузе.
Рассмотрим переход к итерационным процедурам при вычислении чисел
Фибоначчи. В предыдущей работе было приведено классическое дважды
рекурсивное вычисление:
fib(1, 1). // точки остановки
fib(2, 1). // двойной рекурсии
fib(N, Fib):- N>2, // условие выхода
N1=N-1, fib(N1, Fib1), //1-й рекурсивный вызов
N2=N-2, fib(N2, Fib2), //2-й рекурсивный вызов
Fib=Fib1+Fib2. //очередное вычисление числа
Такое вычисление порождает ветвящееся дерево целей на промежуточ-
ных этапах вычислений и приводит к лавинообразному росту объёмов тре-
буемой памяти и вычислительных затрат. С целью уменьшения этих затрат
двойная рекурсия преобразуется в обычную «однократную» рекурсию, с использованием трёхместного предиката fib:

44
fib(1,1,0). // точка остановки рекурсии
fib(N,Fib,Fib1):- N>1, // условие выхода
N1=N-1, // номер предыдущего шага
fib(N1,Fib1,Fib2), // рекурсивный вызов
Fib=Fib1+Fib2. //очередное вычисление числа
Следующий этап — создание итерационной (праворекурсивной) про-
граммы. Для лучшего понимания полезно рассмотреть аналогичную проце-
дуру на императивном языке программирования, например, на ПАСКАЛе:
procedure fib_it(var N,Fib,I,F1,F2: integer);
begin
while I<=N do begin
I:=I+1;
Fib:=F1+F2;
F1:=F2;
F2:=Fib
end
end;
Такая процедура должна вызываться в программе оператором:
fib_it(N,Fib,1,1,0);
где входная переменная N должна содержать номер, а выходная перемен-
ная Fib — значение числа Фибоначчи. При этом входные параметры процедуры fib_it принимают значения:
I:=1; // стартовое значение счётчика цикла
F1:=1; // очередное значение числа
F2:=0; // предыдущее значение
Аналогичное предложенной процедуре вычисление чисел Фибоначчи
на ПРОЛОГе возможно программой с двумя накопителями:

45
fib_it(N, Fib):- fib(N, Fib, 1, 1, 0). // начало итерации
fib(N, Fib, I, F1, F2):- I<N,
NewI=I+1, // номер следующего шага
NewF1=F1+F2,
fib(N, Fib, NewI, NewF1, F1). // «правая» рекурсия
fib(N, Fib, N, Fib,_). //передача информации накопителям (конец итерации)
Рассмотренная процедура может быть упрощена с целью экономии па-
мяти так, что входная переменная N будет передавать в процедуру номер
числа и служить в процедуре счётчиком цикла:
procedure fib_iter(var N,Fib,F1,F2: integer);
begin while N>1 do begin N:=N-1;
Fib:=F1+F2;
F1:=F2;
F2:=Fib
end
end;
Эта процедура будет вызываться в программе на ПАСКАЛе операто-
ром
fib_iter(N,Fib,1,0);
где входная переменная N, выходная переменная Fib аналогичны fib_it.
Аналогичное итерационное (праворекурсивное) вычисление на ПРОЛОГе
выглядит программой с использованием одного накопителя:
fib_iter(N, Fib) :- fib(N, 1, 0, Fib). // начало итерации
fib(N, F1, F2, Fib) :- N>1,
N1= N-1,
NewF1= F1 + F2,
fib(N1, NewF1, F1, Fib). // "правая" рекурсия
fib(1, Fib, _, Fib). // передача информации накопителю
// (конец итерации)

46
% Пример 5. Переход от рекурсивных к итерационным ПРОЛОГ-программам.
predicates
fact(integer, integer)
fact_it(integer, integer)
fact_it(integer, integer, integer, integer)
fact_iter(integer, integer)
fact_iter(integer, integer, integer)
clauses
/* Рекурсивное вычисление факториала по формуле: N! = (N-1)! * N. */
fact(0,1).
fact(1,1).
fact(N,F):- N>1, N1=N-1, fact(N1,F1), F=F1*N.
% Функция итерационного вычисления факториала N!=1*2*3*...*N на ПАСКАЛе:
/* INTEGER FUNCTION FACT_IT(N: INTEGER);
VAR I, T: INTEGER;
BEGIN
I:=0;
T:=1;
REPEAT
I:=I+1;
T:=T*I;
UNTIL I>=N;
FACT_IT:=T;
END; */
/* Итерационное вычисление факториала аналогично функции на Паскале. */
fact_it(N,F) :- fact_it(0,N,1,F).
fact_it(I,N,T,F):- I<N, I1=I+1, T1=T*I1, fact_it(I1,N,T1,F).
fact_it(N,N,F,F).
/* Упрощенная функция вычисления факториала N!=N*(N-1)*...*2*1
/* на ПАСКАЛе:
INTEGER FUNCTION FACT_ITER(N: INTEGER);
VAR T: INTEGER;
BEGIN
T:=N;
REPEAT
N:=N-1;
T:=T*N;
UNTIL N<=1;
FACT_ITER:=T;
END; */
/* Упрощенное итерационное вычисление факториала N!=N*(N-1)*...*2*1. */
fact_iter(N,F) :- fact_iter(N,1,F).
fact_iter(N,T,F):- N>0, T1=T*N, N1=N-1, fact_iter(N1,T1,F).
fact_iter(0,F,F).

47
В примере 5 представлены итерационные аналоги рекурсивной про-
граммы вычисления факториала натурального числа с использованием
различного количества накопителей. Для сравнения в этом примере также
представлены точные аналоги этих вычислительных процедур на императивном языке программирования ПАСКАЛЬ. Итерационные программы до-
статочно сложны, в связи с чем особенно важно выполнение заданий
настоящей лабораторной работы.
В заключение важно заметить, что преобразование рекурсивных правил
к итерационному (праворекурсивному) виду не является последним эта-
пом на пути оптимизации рекурсивных программ. Дальнейшая оптимизация в ряде случаев возможна также при использовании отсечения — спе-
циального приёма, позволяющего изменять процедурный смысл логической
программы. Этому вопросу посвящён материал одной из следующих лабо-
раторных работ.
Задания для работы №5
Часть 1
1. Разработать итерационную программу, реализующую алгоритм Евкли-
да, поиска наибольшего общего делителя двух натуральных чисел.
2. Написать итерационную программу вычисления n-го члена арифмети-
ческой прогрессии.
3. Разработать итерационную программу возведения натуральных чисел
в натуральную степень, использую только операцию умножения.
4. Написать итерационную программу вычисления суммы n членов
арифметической прогрессии.
5. Написать итерационную программу вычисления n-го члена геометри-
ческой прогрессии.
6. Написать итерационную программу вычисления суммы n членов гео-
метрической прогрессии.
7. Создать таблицу «треугольных» чисел, вычислив их итерационным
способом.

48
Часть 2
1. Разработать итерационную программу «предок по прямой мужской ли-
нии».
2. Разработать итерационную программу «предок по прямой женской ли-
нии».
3. Разработать итерационную программу «потомок по прямой мужской
линии».
4. Разработать итерационную программу «потомок».
5. Разработать итерационную программу «потомок по прямой женской
линии».
Часть 3
Переделать программы 3-й части работы №4 из рекурсивных в итераци-
онные.
Методические указания
При выполнении заданий к настоящей лабораторной работе предлагается сначала создать рекурсивную программу, а затем осуществить переход
от рекурсивных к итерационным вычислениям. Рекомендуется сравнить все
варианты вычислений, например, по моменту переполнения стека, о кото-
ром сообщает ПРОЛОГ-машина. При подготовке к выполнению и сдаче ла-
бораторных работ рекомендуется ознакомиться с теоретическим понятием
праворекурсивной логической программы (т.е. с хвостовой рекурсией), изложенным в [8]. Итерационные вычисления и переменные-накопители представлены в [14].
Содержание отчёта:
1) заголовок и цель лабораторной работы;
2) индивидуальное задание на выполнение лабораторной работы;
3) математические формулы, иллюстрирующие решение задачи;
4) текст логической программы;
5) результаты работы итерационных программ для сравнения с ре-
курсивными;
6) выводы.

49
Р а б о т а №6. ПРОГРАММЫ ОБРАБОТКИ СПИСКОВ
Цель работы — изучение логических программ обработки списков на
ПРОЛОГе. Освоение принципов разработки рекурсивных и нерекурсивных
программ работы со списками.
Общие сведения
Материал настоящей работы посвящён программам обработки списков.
При изучении приёмов логического программирования эта динамическая
структура полезна с точки зрения демонстрации возможностей языка
ПРОЛОГ. Рекурсивная структура списков удачно сочетается со структурой
рекурсивных правил. Кроме того, динамическая структура «список» весьма
важна с практической точки зрения при разработке программного обеспечения. Возможности декларативного языка ПРОЛОГ по обработке таких типов
данных на порядок выше возможностей императивных языков программи-
рования, таких как Паскаль, Си или Бэйсик. Только специально ориентиро-
ванный на списковые структуры язык функционального программирования
ЛИСП имеет аналогичные и даже большие возможности, но только для этих
структур.
Встроенный механизм создания списков уже был представлен в работе
№3. Однако не было рассмотрено программ обработки списков. В отсут-
ствии таких программ можно было лишь узнать содержимое элементов
спискового поля. Остались невыясненными, например, ответы на вопросы о
принадлежности элемента списку, длине списка, о подсписке списка, о со-
седних элементах списка и т. п. Не были рассмотрены такие важнейшие
операции как добавление элемента в список, удаление элемента из списка,
слияние списков, обращение списка, перестановка списка, сортировка спис-
ка, разбиение списка по заданному правилу, проверка упорядоченности
списка и т.д. Нельзя было решить и другие задачи со списками, к числу ко-
торых можно отнести, например, дублирование элементов списка, осу-
ществление алгебраических операций с элементами списка, и т.п. Даль-
нейшее изложение посвящено методам решения таких задач и ответам на
подобные вопросы.

50
Список (как было сказано в работе №3) есть двоичная (бинарная) рекур-
сивная структура, то есть структура, имеющая две составляющие. Первая
составляющая есть элемент заданного типа, а вторая составляющая есть
список того же типа, что и определяемый. Конечной точкой этого рекурсив-
ного определения является пустой список — [].
Первый элемент такой бинарной структуры называется «голова», а второй — «хвост» списка:
[Голова| Хвост] или [Head| Tail].
Добавить новый элемент в список можно, например, сделав его головой
нового списка, в то время как список, в который добавляется этот новый
элемент, станет хвостом нового списка.
Удаление элемента из списка также традиционно осуществляется чаще
всего из головы списка. При этом создаётся более короткий список, головой
которого становится голова хвоста исходного списка. Такой способ добавления и удаления особенно удобен при моделировании стека.
Наряду с описанными, возможны и любые другие операции со списками.
Разделение на голову и хвост, между которыми стоит вертикальная чер-
та «|» , — способ стандартного представления списков на ПРОЛОГе:
l([Head| Tail]),
где l(...) — произвольно выбранное имя предиката для хранения списка. Та-
кой синтаксис чаще всего используется в правилах обработки списков, а не
в фактах, осуществляющих хранение списков.
Для создания спискового типа данных в разделе domains следует в пра-
вой части от знака равенства после имени типа элементов списка поставить
знак «*»:
domains number = integer*
Создаваемый список должен быть аргументом некоторого предиката:
predicates l(number)
Встроенный механизм создания списков ПРОЛОГа предполагает представление списка данных с помощью стандартной синтаксической конструкции —
квадратных скобок «[» и» «]». Причём элементы перечисляются через запятую, а
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
