- •Введение
- •1. Составление программ
- •1.1. Основные определения
- •1.2. Декларации в программах
- •1.3. Объявление предикатов и типов их аргументов
- •1.4. Другие разделы программы
- •Упражнения
- •2. Механизмы доказательства правил
- •2.1. Сопоставление с откатом
- •2.2. Рекурсия
- •Упражнения
- •3.Операции в Visual Prolog. Ввод-вывод
- •3.1. Операции
- •3.2. Предикаты ввода-вывода
- •4. Управление процессом доказательства правил
- •4.1. Искусственный откат
- •4.2. Отсечение
- •4.3. Повтор, определяемый пользователем
- •5. Списки
- •5.1. Процедуры обработки списков
- •5.2. Организация стеков и очередей
- •6. Внутренняя база фактов
- •7. Иерархическая организация данных
- •8. Работа с деревьями и графами
- •8.1. Двоичные деревья
- •8.2. Графы
- •9. Работа с именами и строками
- •Заключение
- •Библиографический список
- •Оглавление
2.2. Рекурсия
Составим базу знаний, описывающую фрагмент генеалогического дерева императорской семьи Романовых по мужской линии; факты составлены по шаблону «отец (имя_сына, имя_отца)»:
отец (“Павел I”, “Петр III”).
отец (“Александр I”, “Павел I”).
отец (“Николай I”, “Павел I”).
отец (“Александр II”, “Николай I”).
отец (“Александр III”, “Александр II”).
отец (“Николай II”,“Александр III”).
Предположим, что нам надо убедиться, что один из императоров является предком другого.
Для отношения «дед» правило выглядит так:
дед(X,Z):- отец(X,Y),отец(Y,Z).
(Z – дед X, если Y- отец Х и Z – отец Y)
для прадеда:
прадед(X,W):- отец(X,Y), отец(Y,Z), отец(Z, W).
Очевидно, что для прапрадеда тело правила должно состоять из 4-х предикатов. Чтобы составить достаточно большое количество вариантов родственных отношений надо иметь несколько правил из 2-х, 3-х, 4-х и т.д. предикатов, рассчитанных на цепочки наследования различной длины.
В качестве альтернативного решения создадим рекурсивную процедуру «предок» из двух правил:
предок(X, W) :- отец (X, W). %1
предок(X, W) :- отец(X, Y), предок (Y, W). %2
Процедура декларируется следующим образом:
W является предком X,
если W – отец X (правило 1),
или если существует такое Y, что Y – отец Х и W - предок Y(правило 2).
В любой рекурсивной процедуре должен быть рекурсивный вызов с изменяющимися аргументами и граничное или терминальное условие, при выполнении которого рекурсивные вызовы прекращаются. Рекурсивный вызов содержится во втором правиле процедуры «предок», тогда как первое правило выражает терминальное условие. Аргументы рекурсивного вызова изменяются в результате сопоставления соответствующего предиката правила и фактов БЗ. Фактически рекурсия обеспечивает неоднократный запуск правила «предок».
При доказательстве цели «предок (“Александр III”, “Павел I”)» в памяти системы автоматически создается последовательность утверждений:
отец(“Александр III”, “Александр II”), отец(“Александр II”, “Николай I”), отец(“Николай I”, “Павел I”), отец(“Павел I”, “Петр III”).
Процедура ведет себя по-разному в традиционном Прологе и в VIP. Если VIP останавливается на 1-м найденном варианте решения, т.е. ведет себя как классическая рекурсия, то в традиционном Прологе просматривается вся БЗ. Например, для цели
«предок ( “Николай II”, X )» VIP дает одно решение – “Александр III”, а традиционный Пролог перечисляет 5 решений: “Александр III”, “Александр II”, “Николай I”, “Павел I”, “Петр III” и останавливается при исчерпании всех подходящих фактов «отец/2».
Отметим одно существенное обстоятельство. От изменения порядка перечисления подцелей во втором правиле и перемене мест правил (правило 2 становится первым) декларативный смысл процедуры не изменяется. Однако в вычислительном отношении процедура становится хуже – возрастает время вычислений. Процедура же
предок(X, Z) :- предок (Y, Z), отец(X, Y).
предок(X, Z) :- отец (X, Z).
вообще неработоспособна.
В приведенном выше примере рекурсия использует факты базы знаний. Но в Прологе можно реализовывать рекурсивные алгоритмы без базы знаний, точно так же, как и на других языках. Рассмотрим два примера таких задач.
Алгоритм определения наибольшего общего делителя (НОД) для положительных целых чисел имеет следующий вид:
если X = Y, то НОД = X, или
если X > Y, то определяется НОД двух чисел: X-Y и Y, или
если X < Y, то определяется НОД чисел Y - X и X.
Соответствующая процедура выглядит следующим образом:
нод (X,X,X). % терминальный предикат
нод (X,Y,D):-X > Y, X1 = X - Y, нод (X1, Y, D).
нод (X,Y,D):-X < Y, Y1 = Y - X, нод (X, Y1, D).
(В процедуре используются интуитивно понятные предикаты «>» и «=»; подробно они рассмотрены в подразд. 3.1).
В качестве второго примера выбрана классическая задача «Ханойские башни». Формулируется она следующим образом[3].
Даны 3 стержня и некоторое количество дисков. Диски отличаются диаметром; в середине диска есть отверстие, достаточное для того, чтобы надеть его на стержень. Пусть вначале все диски находятся на левом стержне (рис. 1).
-
Рис.1
Нужно переместить все диски на средний стержень c сохранением порядка их взаимного расположения. Правый стержень можно использовать в качестве промежуточного. Это задача довольно легко решается с 2 или 3 дисками, но при большем количестве дисков процесс решения становится достаточно сложным.
N дисков можно переместить за три этапа:
перемещаем N-1 дисков на правый стержень;
последний (N-й) диск перемещаем на средний стержень;
перемещаем N-1 дисков с правого стержня на средний.
Диски перемещаются с одного стержня на другой по одному; диск большего размера нельзя помещать на меньший диск.
Программа
на
«hanoi» с одним аргументом, показывающим количество перемещаемых дисков;
«передвинуть/4», который определяет перемещение N
дисков с одного стержня на другой с использованием свободного стержня в качестве промежуточного;
inform, выводящий сообщение о перемещении диска.
DOMAINS
левый=symbol
правый=symbol
средний=symbol
PREDICATES
nondeterm hanoi(integer)
nondeterm передвинуть
(integer,symbol,symbol,symbol)
сообщить(symbol,symbol)
CLAUSES
hanoi(N):-передвинуть(N,левый,средний, правый).
передвинуть(0, _,_,_).%терминальный предикат
передвинуть (N,A,B,C):-
N1=N-1, передвинуть (N1, A, C, B),
сообщить( A,B),
передвинуть(N1,C,B,A).
сообщить(X, Y):-
write("Передвинули диск с",X,"на",Y), nl.
GOAL
hanoi(3).
Результат будет таким:
Передвинули диск с левый на средний
Передвинули диск с левый на правый
Передвинули диск с среднего на правый
Передвинули диск с левый на средний
Передвинули диск с правого на левый
Передвинули диск с правого на средний
Передвинули диск с левый на средний
В процедуре два рекурсивных вызова «передвинуть». Предикат «сообщить» и «передвинуть (N1,C,B,A)» вызываются после того, как предикат «передвинуть (N1, A, C, B)» оказался доказанным: при уменьшении параметра N было достигнуто терминальное условие.
Рекурсивные процедуры наиболее эффективны при работе со структурами данных, имеющих рекурсивную природу, таких как списки и деревья (см. разд. 5 и 8).
