
Логическое программирование1 / 1-5_LR_4KSM_Logichne_progr_2014-15
.pdf121
Г.5.3. Повышение эффективности конкатенации списков за счет совершенствования структуры данных
До сих пор в программах конкатенация была определена так:
конк( [ ], L, L).
конк( [X | L1], L2, [X | L3] ) :- конк( L1, L2, L3 ).
Эта процедура неэффективна, если первый список - длинный. Следующий пример объясняет, почему это так:
?- конк( [а, b, с], [d, e], L).
Этот вопрос порождает следующую последовательность целей:
конк( [а, b, с], [d, e], L) |
|
конк( [b, с], [d, e], L') |
где L = [a | L'] |
конк( [с], [d, e], L") |
где L' = [b | L"] |
конк( [ ], [d, e], L'") |
где L" = [c | L'''] |
true (истина) |
где L'" = [d, е] |
Программа фактически сканирует весь первый список, пока не обнаружит его конец.
А нельзя ли было бы проскочить весь первый список за один шаг и сразу подсоединить к нему второй список, вместо того, чтобы постепенно продвигаться вдоль него?
Для этого необходимо знать, где расположен конец списка, а, следовательно, мы нуждаемся в другом его представлении.
Один из вариантов - представлять список парой списков.
Например, список [а, b, с] можно представить следующими двумя списками: L1 = [a, b, c, d, e] и L2 = [d, e].
Подобная пара списков, записанная для краткости как L1-L2, представляет собой разность между L1 и L2.
Это представление работает при том условии, что L2 - "конечный участок" списка L1.

122
Один список может быть представлен несколькими разностными парами.
Поэтому список [а, b, с] можно |
представить |
как [а, |
b, с]-[ ] |
или |
|
[a, b, c, d, e]-[d, e] |
или [a, b, c, d, e |
| T]-[d, e | |
T] или |
[а, b, с | |
Т]-Т, |
где Т - произвольный |
список, и т.п. |
|
|
|
|
Пустой список представляется любой парой L-L.
Поскольку второй элемент указывает на конец списка, этот конец доступен сразу, что можно использовать для эффективной реализации конкатенации.
Метод показан на рисунке Г.1.
Рисунок Г.1. Конкатенация списков, представленных в виде разностных пар (L1 представляется как A1-Z1, L2 как A2-Z2 и результат L3 - как A1-Z2; при этом должно выполняться равенство Z1 = А2)
Соответствующее отношение конкатенации записывается на Прологе в виде факта конкат( A1-Z1, Z1-Z2, A1-Z2).
Используем конкат для конкатенации двух списков:
-списка [а, b, с], представленного парой [а, b, с | Т1]-Т1;
-списка [d, e], представленного парой [d, e | Т2]-Т2 .
?- конкат( [а, b, с | Т1]-T1, [d, е | Т2]-Т2, L ).
Оказывается, что для выполнения конкатенации достаточно простого сопоставления этой цели с предложением конкат.
Результат сопоставления:
T1 = [d, e | Т2]
L = [a, b, c, d, e | T2]-T2
123
Г.5.4. Повышение эффективности зa счет добавления вычисленных фактов к базе данных
Иногда в процессе вычислений приходится одну и ту же цель достигать снова и снова.
Поскольку в Прологе отсутствует специальный механизм выявления этой ситуации, соответствующая цепочка вычислений каждый раз повторяется заново.
В качестве примера, рассмотрим программу вычисления N-го числа Фибоначчи для некоторого заданного N.
Последовательность Фибоначчи имеет вид: 1, 1, 2, 3, 5, 8, 13, ...
Каждый член последовательности, за исключением первых двух, представляет собой сумму предыдущих двух членов.
Для вычисления N-гo числа Фибоначчи F определим предикат фиб(N, F). Нумерацию чисел последовательности начнем с N = 1.
Программа для фиб обрабатывает сначала первые два числа Фибоначчи как особые случаи, а затем определяет общее правило построения последовательности Фибоначчи.
фиб( 1, 1). % 1-е число Фибоначчи фиб( 2, 1). % 2-е число Фибоначчи
фиб( N, F) :-
% N-е число Фиб., N > 2
N > 2, N1 is N - 1, фиб( N1, F1), N2 is N - 2, фиб( N2, F2), F is F1 + F2. % N-e число есть сумма двух предыдущих
Процедура фиб имеет тенденцию к повторению вычислений. Это легко увидеть, если трассировать цель ?- фиб( 6, F). На рисунке Г.2 показано, как протекает этот вычислительный процесс.
Например, третье число Фибоначчи f(3) понадобилось в трех местах, и были повторены три раза одни и те же вычисления.

124
Рисунок Г.2. Вычисление 6-го числа Фибоначчи процедурой фиб
Повторения легко избежать, если запоминать каждое вычисленное число. Идея состоит в применении встроенной процедуры assert для добавления
промежуточных результатов в базу данных в виде фактов (они должны предшествовать другим предложениям, чтобы предотвратить применение общего правила в случаях, когда результат уже известен). Усовершенствованная процедура фиб2 отличается от фиб только этим добавлением.
фиб2( 1, 1). |
% 1-е число Фибоначчи |
фиб2( 2, 1). |
% 2-е число Фибоначчи |
фиб2( N, F) :- |
% N-e число Фиб., N > 2 |
N > 2, |
Nl is N - 1, фиб2( N1, F1), N2 is N - 2, фиб2( N2, F2), |
|
F is F1 + F2, |
% N-e число есть сумма двух предыдущих |
|
asserta( фиб2( N, F) ). |
% Запоминание N-го числа |

125
Эта программа, при попытке достичь какую-либо цель, будет смотреть сперва на накопленные об этом отношении факты, и только после этого применять общее правило.
В результате, после вычисления цели фиб2( N, F), все числа Фибоначчи вплоть до N-го будут сохранены.
На рисунке Г.3 показан процесс вычислении 6-го числа при помощи фиб2.
Рисунок Г.3. Вычисление 6-го числа Фибоначчи при помощи процедуры фиб2, которая запоминает предыдущие результаты
(по сравнению с процедурой фиб, здесь вычислений меньше (см. рисунок Г.2))
Сравнение рисунков Г.3 и Г.2 показывает, насколько уменьшилась вычислительная сложность. Для больших N, такое уменьшение еще ощутимее.

126
Запоминание промежуточных результатов - стандартный метод, позволяющий избегать повторных вычислений.
В случае чисел Фибоначчи, повторных вычислений можно избежать применением другого алгоритма (а не только запоминанием промежуточных результатов). Такой алгоритм позволяет создать программу, более трудную для понимания, но и более эффективную.
Идея состоит не в том, чтобы определить N-e число Фибоначчи как сумму своих предшественников по последовательности, оставляя рекурсивным вызовам организовать вычисления "сверху вниз", вплоть до первых двух чисел.
Вместо этого можно работать "снизу вверх": начать с первых двух чисел и продвигаться вперед, вычисляя члены последовательности один за другим.
Остановиться нужно в тот момент, когда будет достигнуто N-e число. Большая часть работы в такой программе выполняется процедурой
фибвперед( М, N, F1, F2, F)
Здесь F1 и F2 - (М - 1)-е и М-е числа, а F - N-e число Фибоначчи. Рисунок Г.4 помогает понять отношение фибвперед.
Рисунок Г.4. Отношения в последовательности Фибоначчи (конфигурация изображается здесь в виде большого круга и определяется тремя параметрами:
индексом М и двумя последовательными числами f(M-1) и f(М))
127
В соответствии с рисунком, фибвперед находит последовательность преобразований для достижения конечной конфигурации (М = N) из некоторой заданной начальной конфигурации. При запуске фибвперед, все аргументы, кроме F, должны быть конкретизированы, а М - меньше или равно N.
Вот эта программа:
фиб3( N, F) :- фибвперед(2, N, 1, 1, F).
% Первые два числа Фибоначчи равны 1
фибвперед( М, N, F1, F2, F2) :- |
М >= N. |
% N-e число достигнуто |
фибвперед( M, N, F1, F2, F) :- |
M < N, |
% N-e число еще не достигнуто |
СледМ is М + 1, СледF2 is F1 + F2, |
|
|
фибвперед(СледМ, N, F2, СледF2, F). |
|
УПРАЖНЕНИЯ ДЛЯ САМОПРОВЕРКИ
Упражнение Г.1. Показанные ниже процедуры подсп1, подсп2 и подсп3
реализуют отношение получения подсписка: подсп1 имеет в основном процедурное определение, а подсп2 и подсп3 написаны в декларативном стиле.
Изучите поведение процедур на примерах нескольких списков, обращая внимание на эффективность работы: какие две из них имеют одинаковое поведение и эффективность и почему третья менее эффективна?
подсп1( Спис, Подспис) :- начало( Спис, Подспис). подсп1( [ _ | Хвост], Подспис) :- подсп1( Хвост, Подспис). % Подспис - подсписок хвоста
начало( _, [ ]).
начало( [X | Спис1], [X | Спис2] ) :- начало(Спис1, Спис2).
подсп2(Спис, Подспис) :- |
|
конк(Спис1, Спис2, Спис), |
конк( Спис3, Подспис, Cпис1). |
подсп3(Спис, Подспис) :- |
|
конк(Спис1, Спис2, Спис), |
конк(Подспис, _, Спис2). |
128
Упражнение Г.2. Определите отношение добавить_в_конец(Список, Элемент, НовыйСписок), добавляющее Элемент в конец списка Список, с
получением результата НовыйСписок. Оба списка представляйте разностными парами.
Упражнение Г.3. Определите отношение обратить(Список, ОбращенныйСписок), где оба списка представлены разностными парами.
Упражнение Г.4. Перепищите процедуру собрать из подраздела Г.5.2, используя разностное представление списков, чтобы конкатенация выполнялась эффективнее.
РЕЗЮМЕ
Для оценки качества программы существует несколько критериев:
правильность; эффективность; простота и читабельность; удобство модификации; документированность.
Принцип пошаговой детализации - хороший способ организации процесса разработки программ, который применим к отношениям, алгоритмам и структурам данных.
Следующие методы часто помогают находить идеи для совершенствования программ на Прологе:
-применение рекурсии (выявить граничные и общие случаи рекурсивного определения);
-обобщение (рассмотреть более общую задачу, которую проще решить, чем исходную);
-использование рисунков (графическое представление помогает в выявлении важных отношений).
Полезно следовать некоторым стилистическим соглашениям для уменьшения опасности внесения ошибок в программы и создания программ, легких для чтения, отладки и модификации.
129
В Пролог-системах обычно имеются средства отладки.
Наиболее полезными являются средства трассировки программ.
Существует много способов повышения эффективности Прологпрограммы.
Наиболее простые способы включают в себя: изменение порядка целей и предложений; управляемый перебор при помощи отсечений; запоминание (с помощью assert) решений, которые иначе пришлось бы перевычислять.
Более тонкие и радикальные методы связаны с улучшением алгоритмов (особенно, в части повышения эффективности перебора) и с совершенствованием структур данных.