Повышение эффективности конкатенации списков за счет совершенствования структуры данных



 

До сих пор в наших программах конкатенация была определена так:

конк( [], 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, e]

Ясно, что программа фактически сканирует весь первый список, пока не обнаружит его конец.

А нельзя ли было бы проскочить весь первый список за один шаг и сразу подсоединить к нему второй список, вместо того, чтобы постепенно продвигаться вдоль него? Но для этого необходимо знать, где расположен конец списка, а следовательно, мы нуждаемся в другом его представлении. Один из вариантов — представлять список парой списков. Например, список

[а, b, с]

можно представить следующими двумя списками:

L1 = [a, b, c, d, e]

L2 = [d, e]

Подобная пара списков, записанная для краткости как L1-L2, представляет собой "разность" между L1 и L2. Это представление работает только при том условии, что L2 — "конечный участок" списка L1. Заметим, что один и тот же список может быть представлен несколькими "разностными парами". Поэтому список [а, b, с] можно представить как

[а, b, с]-[]

или

[a, b, c, d, e]-[d, e]

или

[a, b, c, d, e | T]-[d, e | T]

или

[а, b, с | T]-T

где T — произвольный список, и т.п. Пустой список представляется любой парой L-L.

Поскольку второй член пары указывает на конец списка, этот конец доступен сразу. Это можно использовать для эффективной реализации конкатенации. Метод показан на рис. 8.1. Соответствующее отношение конкатенации записывается на Прологе в виде факта

конкат( A1-Z1, Z1-Z2, A1-Z2).

Давайте используем конкат для конкатенации двух списков: списка [а, b, с], представленного парой [а, b, с | Т1]-Т1, и списка [d, e], представленного парой [d, e | Т2]-Т2:

?- конкат( [а, b, с | Т1]-T1, [d, e | Т2]-Т2, L ).

Оказывается, что для выполнения конкатенации достаточно простого сопоставления этой цели с предложением конкат. Результат сопоставления:

T1 = [d, e | Т2]

L = [a, b, c, d, e | T2]-T2

 

Рис. 8.1. Конкатенация списков, представленных в виде разностных пар. L1 представляется как A1-Z1, L2 как A2-Z2 и результат L3 — как A1-Z2. При этом должно выполняться равенство Z1 = А2.

 

Повышение эффективности зa счет добавления вычисленных фактов к базе данных

 

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

В качестве примера рассмотрим программу вычисления N-го числа Фибоначчи для некоторого заданного N. Последовательность Фибоначчи имеет вид:

1, 1, 2, 3, 5, 8, 13, …

Каждый член последовательности, за исключением первых двух, представляет собой сумму предыдущих двух членов. Для вычисления N-гo числа Фибоначчи F определим предикат

фиб( N, F)

Нумерацию чисел последовательности начнем с N = 1. Программа для фиб обрабатывает сначала первые два числа Фибоначчи как два особых случая, а затем определяет общее правило построения последовательности Фибоначчи:

фиб( 1, 1). % 1-e число Фибоначчи

фиб( 2, 1). % 2-e число Фибоначчи

фиб( N, F) :- % N-e число Фиб., N > 2

N > 2,

N1 is N - 1, фиб( N1, F1),

N2 is N - 2, фиб( N2, F2),

F is F1 + F2. % N-e число есть сумма двух

% предыдущих

Процедура фиб имеет тенденцию к повторению вычислений. Это легко увидеть, если трассировать цель

?- фиб( 6, F).

На рис. 8.2 показано, как протекает этот вычислительный процесс. Например, третье число Фибоначчи f( 3) понадобилось в трех местах, и были повторены три раза одни и те же вычисления.

Этого легко избежать, если запоминать каждое вновь вычисленное число. Идея состоит в применении встроенной процедуры assert для добавления этих (промежуточных) результатов в базу данных в виде фактов. Эти факты должны предшествовать другим предложениям, чтобы предотвратить применение общего правила в случаях, для которых результат уже известен. Усовершенствованная процедура фиб2 отличается от фиб только этим добавлением:

фиб2( 1, 1). % 1-e число Фибоначчи

фиб2( 2, 1). % 2-e число Фибоначчи

фиб2( N, F) :- % N-e число Фиб., N > 2

N > 2,

N1 is N - 1, фиб2( N1, F1),

N2 is N - 2, фиб2( N2, F2),

F is F1 + F2, % N-e число есть сумма

% двух предыдущих

asserta( фиб2( N, F) ). % Запоминание N-го числа

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

Запоминание промежуточных результатов — стандартный метод, позволяющий избегать повторных вычислений. Следует, однако, заметить, что в случае чисел Фибоначчи повторных вычислений можно избежать еще и применением другого алгоритма, а не только запоминанием промежуточных результатов.

 

Рис. 8.2. Вычисление 6-го числа Фибоначчи процедурой фиб.

 

Рис. 8.3. Вычисление 6-го числа Фибоначчи при помощи процедуры фиб2, которая запоминает предыдущие результаты. По сравнению с процедурой фиб здесь вычислений меньше (см. рис. 8.2).

Этот новый алгоритм позволяет создать программу более трудную для понимания, зато более эффективную. Идея состоит на этот раз не в том, чтобы определить N-e число Фибоначчи просто как сумму своих предшественников по последовательности, оставляя рекурсивным вызовам организовать вычисления "сверху вниз" вплоть до самых первых двух чисел. Вместо этого можно работать "снизу вверх": начать с первых двух чисел и продвигаться вперед, вычисляя члены последовательности один за другим. Остановиться нужно в тот момент, когда будет достигнуто N-e число. Большая часть работы в такой программе выполняется процедурой

фибвперед( М, N, F1, F2, F)

Здесь F1 и F2 — (М – 1)-e и М-e числа, а F — N-e число Фибоначчи. Рис. 8.4 помогает понять отношение фибвперед. В соответствии с этим рисунком фибвперед находит последовательность преобразований для достижения конечной конфигурации (в которой М = 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).

 

Рис. 8.4. Отношения в последовательности Фибоначчи. "Конфигурация" изображается здесь в виде большого круга и определяется тремя параметрами: индексом М и двумя последовательными числами f( M-1) и f( М).

 

Упражнения

 

8.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).

8.2. Определите отношение

добавить_в_конец( Список, Элемент, НовыйСписок)

добавляющее Элемент в конец списка Список; результат — НовыйСписок. Оба списка представляйте разностными парами.

8.3. Определите отношение

обратить( Список, ОбращенныйСписок)

где оба списка представлены разностными парами.

8.4. Перепишите процедуру собрать из разд. 8.5.2, используя разностное представление списков, чтобы конкатенация выполнялась эффективнее.

 

 

Резюме

 

• Для оценки качества программы существует несколько критериев:

правильность

эффективность

простота, читабельность

удобство модификации

документированность

• Принцип пошаговой детализации — хороший способ организации процесса разработки программ. Пошаговая детализация применима к отношениям, алгоритмам и структурам данных.

• Следующие методы часто помогают находить идеи для совершенствования программ на Прологе:

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

Обобщение : рассмотреть такую более общую задачу, которую проще решить, чем исходную.

Использование рисунков : графическое представление помогает в выявлении важных отношений.

• Полезно следовать некоторым стилистическим соглашениям для уменьшения опасности внесения ошибок в программы и создания программ, легких для чтения, отладки и модификации.

• В пролог-системах обычно имеются средства отладки. Наиболее полезными являются средства трассировки программ.

• Существует много способов повышения эффективности программы. Наиболее простые способы включают в себя:

изменение порядка целей и предложений

управляемый перебор при помощи введения отсечений

запоминание (с помощью assert) решений, которые иначе пришлось бы перевычислять

Более тонкие и радикальные методы связаны с улучшением алгоритмов (особенно, в части повышения эффективности перебора) и с совершенствованием структур данных.

 

Часть 2


Дата добавления: 2018-05-01; просмотров: 383; Мы поможем в написании вашей работы!

Поделиться с друзьями:






Мы поможем в написании ваших работ!