Ответ на вопрос
Код:
parent(alice, bob).
parent(bob, charlie).
ancestor(X,Y) :- parent(X,Y).
ancestor(X,Y) :- parent(X,Z), ancestor(Z,Y).1) Что делает этот кусокparent/2 — факты: alice — родитель bob, bob — родитель charlie.ancestor/2 — рекурсивное правило: Y — потомок X, если либо X — прямой родитель Y (первая альтернатива), либо существует Z такой, что X — родитель Z и Z — предок Y (вторая альтернатива).2) Поведение при запросе ?- ancestor(alice, Y).
Prolog применит поиск по порядку (глубинный поиск с возвратами):Попытается первая альтернатива: ancestor(X,Y) :- parent(X,Y).
X = alice, parent(alice, Y) даёт Y = bob. Это первое решение: Y = bob.При дальнейшем backtracking прямых parent(alice, ?) больше нет.Переходит ко второй альтернативе: ancestor(X,Y) :- parent(X,Z), ancestor(Z,Y).
берёт parent(alice,Z) → Z = bob, затем вызывает ancestor(bob,Y).внутри ancestor(bob,Y) сначала проверяет parent(bob,Y) → Y = charlie. Это второе решение: Y = charlie.далее для ancestor(bob, Y) нет других parent(bob, ?), пробует вторую альтернатива — нет Z, приводящего к новым результатам.нет других Z для parent(alice,Z), поэтому поиск заканчивается.Итого при этом примере ответы будут в порядке: Y = bob ; Y = charlie.3) Дубли, рекурсия и зацикливаниеДубликаты: если существуют разные пути из X в один и тот же Y (разные промежуточные Z), то Prolog вернёт Y несколько раз (по одному решению на каждый путь). Пример:
parent(a,b). parent(a,c). parent(b,d). parent(c,d).
Тогда ?- ancestor(a,Y). вернёт Y = b ; Y = c ; Y = d ; Y = d. (d дважды — через b и через c).Зацикливание: если граф родителей содержит цикл (пример parent(a,b). parent(b,a).), то чистая рекурсия может привести к бесконечному погружению (бесконечный поиск) и никогда не вернёт все решения. В приведённом исходном примере циклов нет, поэтому поиск завершается.4) Ограничение экспоненциального роста и оптимизации
Ниже — проверенные приёмы, чтобы уменьшить дубли, избежать бесконечных циклов и сократить избыточную рекомпутацию.a) Устранение дубликатов на уровне результатовПолучить уникальные результаты: setof/3 или bagof + sort:
setof(Y, ancestor(alice,Y), Ys). % вернёт упорядоченный список уникальных YИли собрать все и удалить повторы: findall(Y, ancestor(alice,Y), L), sort(L, Unique).b) Отсечение лишних ветвей (cut), но осторожноМожно локально использовать ! чтобы избежать лишних альтернатив, но cut изменяет семантику и может отбросить корректные решения, если неправильно применён.c) Защита от циклов — "visited" аккумуляторДобавить параметр посещённых вершин, чтобы не заходить в уже просмотренные:
ancestor(X,Y) :- ancestor(X,Y,[X]).
ancestor(X,Y,_) :- parent(X,Y).
ancestor(X,Y,Visited) :-
parent(X,Z),
+ member(Z, Visited),
ancestor(Z,Y,[Z|Visited]).Это предотвращает бесконечные циклы в графе.d) Таблинг / мемоизация (рекомендую)В современных Prolog'ах (XSB, SWI-Prolog и др.) есть табличное вычисление (tabling). Оно:
предотвращает бесконечное зацикливание для многих левых рекурсий,сохраняет уже вычисленные ответы и повторно использует их (убирает экспоненциальное повторение).В SWI-Prolog:
:- table ancestor/2.
Это часто самый простой и эффективный способ для транситивных замыканий (reachability).В системах без таблинга можно реализовать кеширование вручную с динамическим предикатом (assert/retract), но это более хрупко.e) Порядок литералов и неудачные вариантыОчень важен порядок в теле правила: ancestor(X,Y) :- parent(X,Z), ancestor(Z,Y). Здесь рекурсивный вызов стоит последним (хвостовая рекурсия) — это позволяет компиляторам выполнять last-call optimization и не накапливать лишний стек при глубоком, но линейном пути.Перестановка: ancestor(X,Y) :- ancestor(Z,Y), parent(X,Z). — приведёт к леворекурсивному варианту и часто к бесконечному рекурсивному вызову (нереализуемо с DFS), поэтому порядок литералов критичен.f) Альтернативы поиска: ширина, итеративное углублениеЕсли нужен ближайший предок (минимальная длина пути), глубинный поиск не гарантирует кратчайший путь. Можно реализовать поиск в ширину (BFS) или итеративное углубление, но это сложнее реализовать в стандартном Prolog; таблинг также может помочь с поиском кратчайших путей если дополнить взвешивание/ограничения.g) Индексация фактовСовременные Prolog-системы индексируют аргументы предикатов (чаще первый, иногда первые два). При частых запросах ancestor(alice, ...) важно, чтобы parent/2 имел индекс по первому аргументу — стандартные реализации это делают, что делает parent(alice, Z) быстрым.5) Резюме рекомендацийДля простых ацикличных деревьев текущая программа адекватна и выдаст корректные ответы.Чтобы убрать дубли: собирать результаты setof/sort.Чтобы избежать циклов и экспоненциального роста в общем графе: использовать таблинг (:- table ancestor/2) — лучший вариант; либо явный список посещённых вершин.Проверяйте порядок литералов: рекурсивный вызов в конце — хорошо (tail-call); леворекурсия может приводить к не-терминированию.Для больших баз данных убедитесь, что parent/2 индексируется и используйте таблинг или кеширование.Если хотите, могу:показать конкретный пример с циклом и как visited предотвращает бесконечность;продемонстрировать использование :- table ancestor/2 в SWI-Prolog;или показать, как убрать дубликаты конкретным примером setof/3.
Еще