|
Лекция
5 |
|
|
|
Отсечение. Сортировка списков. |
|
|
5.1.1 Графическая
иллюстрация действия cut.
5.1.3 Применение cut при
выборе альтернатив.
5.1.4 Формальное
описание действия отсечения.
5.2.2 Добавление
элемента без дублирования.
5.2.4 Отсечение в
численной рекурсии.
5.2.5 Замечания при
использовании отсечения.
5.3.1 Метод наивной
сортировки.
5.3.4 Быстрая сортировка
quick.
В процессе вычисления цели пролог
проводит перебор вариантов, в соответствии с установленным порядком. Цели
выполняются последовательно, одна за другой, а в случае неудачи происходит откат
к предыдущей цели (backtracing).
Однако для повышения эффективности
выполнения программы, часто требуется вмешаться в перебор, ограничить его,
исключить некоторые цели.
Для этой цели в пролог введена
специальная конструкция cut -
"отсечение", обозначаемая как "!"
( Читается "cut", "кат").
Cut подсказывает прологу "закрепить" все
решения предшествующие появлению его в предложении. Это означает, что если
требуется бэктрекинг, то он приведет к неудаче (fail) без попытки поиска
альтернатив.
Графически действие cut можно
показать с помощью box-представления
логического вывода.
В обычном случае бэктрекинг для
правила
|
G:-A,B,C. |
выглядит следующим образом:
Т.е. поиск альтернатив производится
для всех целей: G,A,B,C. Заменим B на cut :
Графическая иллюстрация действия cut. |
|
Действие cut заключается в отмене
поиска альтернатив для целей A,G, стоящих после "!".
Пусть база данных выглядит следующим
|
data(one). |
Процедура для проверки:
|
cut_test_a(X):- data(X). |
Цель:
|
?- cut_test_a(X),nl,write(X).
|
|
|
|
|
Теперь поставим cut в правило:
|
cut_test_b(X):- data(X),!. |
Цель:
|
?-cut_test_b(X),nl,write(X).
|
|
|
|
|
Происходит остановка бэктрекинга для
левого data и родительского
|
cut_test_b(X). |
Теперь поместим cut между двумя
целями.
|
cut_test_с(X,Y):- data(X), |
Теперь бэктрекинг не будет для для
левого data и родительского cut_test_b(X),но будет для правого data,стоящего
после !.
|
?-cut_test_с(X,Y),nl,write(X-Y).
|
|
|
|
|
Рассмотрим функцию Y(X):
/ ⌠ 0 , X < 3 ⌠ Y= < 2 , 3 = < X < 6 ⌠ ⌠ 4 , X >= 6 \ |
|
|
|
|
|
На прологе это запишется через
бинарное отношение f(X, Y).
Процедура выглядит:
|
f(X, 0):- X < 3. |
Зададим вопрос:
|
?-f(1,Y),Y>2. |
|
|
|
|
Таким образом последовательно
проверяются все три предложения, хотя сразу ясно, что выполняется только одно.
Как убрать неэффективность?
Надо использовать отсечение-cut.
Перепишем
|
f( X, 0):-X<3, !. |
! указывает, что возврат из этой точки
проводить не надо.
Что произойдет теперь?
Для цели
|
?-f(1, Y), Y>2. |
|
|
|
|
После выполнения цели X<3
цель Y>2, не достигается, но откат не может произойти, так как стоит cut.
Таким образом сокращается перебор.
Аналогично для цели
|
?-f(5, Y), Y=0. |
|
|
|
|
Здесь введение cut повышает
эффективность программы, сокращая время перебора и объем памяти, не влияет на
декларативное чтение программы.
После изъятия ! декларативный
смысл не изменится.
Такое применение cut называют "зеленым отсечением".
"Зеленые отсечения" лишь отбрасывают те пути вычисления, которые
не приводят к новым решениям.
Бывают и "красные
отсечения", при изъятии которых меняется декларативный смысл
программы.
Рассмотрим предложение
Н:-B1, B2,..., Bm, !,..., Bn.
Это предложение активизируется, когда
некоторая цель G, будет сопоставляться с H.
Тогда G называют целью-родителем.
Если B1, B2,..., Bm выполнены,
а после !, например в Bi, i>m, произошла неудача и требуется
выбрать альтернативные варианты, то для B1, B2,..., Bm такие
альтернативы больше не рассматриваются и все выполнение окончится неудачей.
Кроме этого G будет связана с головой H, и другие предложения
процедуры во внимание не принимаются.
Т.е. отсечение в теле предложения
отбрасывает все предложения , расположенные после этого предложения.
Формально действие отсечения
описывается так:
Пусть цель -родитель сопоставляется с
головой предложения, в теле которого содержится отсечение.
Когда при просмотре целей тела
предложения встречается в качестве цели отсечение, то такая цель считается
успешной и все альтернативы принятым решениям до отсечения отбрасываются и
любая попытка найти новые альтернативы на промежутке между целью-родителем и сut
оканчиваются неудачей.
Процесс поиска возвращается к последнему выбору, сделанному перед
сопоставлением цели родителя.
Найти минимальный элемент из двух.
|
minimum(X, Y, X):- X<=Y, !. |
|
?-minimum(2, 5, Y). |
|
|
|
|
Чтобы добавить элемент в голову списка
достаточно использовать
add(X, L, [X|L]).
Но если возникает необходимость
добавлять только, если элемент отсутствует, то можно добавить правило:
|
add(X, L, L):-member(X, L), !. add(X, L, [X|L]). |
Вопрос
|
?-add(a, [b, c], L).
|
|
|
|
|
Если сечение убрать, то
|
?-add(b, [b, c], L) |
|
|
|
|
Таким образом, при изъятии отсечения,
изменился декларативный смысл программы- отсечение "красное".
Используя отсечение, легко произвести
классификацию объектов.
Например, надо классифицировать числа
|
class(X, plus):-X>0, !. |
|
?-class(4, Y). |
|
|
|
|
Применение отсечения в рекурсии позволяет
значительно сократить использование памяти.
Определим факториал
|
factorial(0, 1):-!. |
Применение сечения за счет сокращения
перебора позволяет повысить эффективность программы. Кроме этого отсечение
упрощает программирование выбора вариантов.
Вместе с тем, часто, при использовании
красных отсечений может изменится декларативный смысл программы.
Поэтому отсечение требует осторожности
в использовании.
Следует избегать двух ошибок:
До сих пор средства пролога
использовались при записи простых процедур и примеров. Рассмотрим более сложные
примеры написания программ на прологе для различных методов сортировки.
В этом методе элементы в списке
переставляются ( перемешиваются permutation/2 ),и проверяется отсортирован
этот список или нет: sorted/1.
Это записывается:
|
sortn(L1, L2) :- permutation(L1, L2), sorted(L2), !.
|
Перестановки определим через append
|
permutation(L, [H|T]) :- append(V, [H|U], L), |
Проверка того, что элементы списка
расположены в возрастающем порядке выражается через два предложения. Факт
означает, что список из одного элемента всегда упорядочен. Правило утверждает,
что список упорядочен ,если первые два элемента расположены по порядку и
остаток ,начинающийся со второго элемента тоже упорядочен:
|
sorted([L]). |
Это простой и эффективный метод.
Декларативное
описание:
|
busort(L, S) :- swap(L, M), !, busort(M, S). |
Декларативное
описание:
Для того чтобы упорядочить непустой
список L=[X|T] необходимо:
|
insort([], []).
|
Описание метода:
Убираем первый элемент:
5 3 7 8 1 4 7 6
получаем: X =5.
и оставшийся список:
3 7 8 1 4 7 6
Разбиваем новый список
на два, помещая в первый элементы меньше X, а во второй - больше X:
( X: 3 1 4 ) X: 7 8 7 6
Сортируем оба списка:
1 3 4 6 7 7 8
Соединим первый
список, X, второй список.
1 3 4 5 6 7 7 8
Декларативное описание:
|
qsort([], []). qsort([H|Tail], S) :- split(H, Tail, Small, Big), qsort(Small, Small1), qsort(Big, Big1), append(Small1, [H|Big1], S).
order(X, Y) :- X =< Y. split(H, [A|Tail], [A|Small], Big) :- order(A, H), !, split(H, Tail, Small, Big). split(H, [A|Tail], Small, [A|Big]) :- split(H, Tail, Small, Big). split(_, [], [], []). |
(c) M.N.Morozov, 1999.