Рассмотрите следующий фрагмент Prolog-кода: parent(alice, bob). parent(bob, carol). ancestor(X,Y) :- parent(X,Y). ancestor(X,Y) :- parent(X,Z), ancestor(Z,Y). Объясните, как работает поиск в этом примере, что такое backtracking и как можно изменить правила, чтобы избежать бесконечной рекурсии при добавлении циклических фактов.
что происходит при поиске и почему может возникнуть зацикливание:
1) Как Prolog ищет ответы SLD,левостороннийпоиск,глубинныйобходSLD, левосторонний поиск, глубинный обходSLD,левостороннийпоиск,глубинныйобход
Prolog пытается доказать цель запросзапросзапрос по правилам сверху вниз и слева направо.Для цели ancestorA,BA,BA,B он сначала подставляет первую клаузу ancestorX,YX,YX,Y :- parentX,YX,YX,Y. — это простой факт: он проверяет, есть ли parentA,BA,BA,B в базе фактов.Если первый путь не дал ответа, он пробует вторую клаузу ancestorX,YX,YX,Y :- parentX,ZX,ZX,Z, ancestorZ,YZ,YZ,Y. — сначала ищет parentA,ZA,ZA,Zберёткакое‑тоZберёт какое‑то Zберёткакое‑тоZ, затем рекурсивно пытается доказать ancestorZ,BZ,BZ,B.При неудаче какой‑то альтернативы Prolog делает backtracking: отменяет последние привязки переменных и пробует следующую альтернативную ветку следующийфактparent/2,следующийспособдоказатьпобочнуюцельилиследующуюклаузуancestor/2следующий факт parent/2, следующий способ доказать побочную цель или следующую клаузу ancestor/2следующийфактparent/2,следующийспособдоказатьпобочнуюцельилиследующуюклаузуancestor/2.
Пример трассировки для запроса ancestoralice,carolalice, carolalice,carol:
Проверяется parentalice,carolalice, carolalice,carol — нет.Переходим ко второй клаузе: ищем parentalice,Zalice, Zalice,Z — находим Z = bob.Теперь доказать ancestorbob,carolbob, carolbob,carol: Проверяем parentbob,carolbob, carolbob,carol — да, успех. Значит ancestoralice,carolalice, carolalice,carol — успех.При дальнейшем backtracking Prolog может искать дополнительные Z для parentalice,Zalice, Zalice,Z и т.д.
2) Что такое backtracking
Backtracking — механизм отката привязок переменных и перебора альтернативных вариантов доказательства, когда текущая ветка не приводит к успеху или когда нужно найти дополнительные ответы. Prolog запоминает точку выбора choicepointchoice pointchoicepoint и при неудаче возвращается к ней, пробуя следующую возможность.
3) Почему возникает бесконечная рекурсия и как её избежать
Алгоритм Prolog реализует глубинный поиск. При наличии циклов в графе отношений parent напримерparent(a,b).parent(b,a).например parent(a,b). parent(b,a).напримерparent(a,b).parent(b,a). и если искомая цель недостижима, глубинный рекурсивный переход может зациклиться: ancestora,Targeta,Targeta,Target → parenta,ba,ba,b → ancestorb,Targetb,Targetb,Target → parentb,ab,ab,a → ancestora,Targeta,Targeta,Target → ...Чтобы избежать зацикливания, нужно не допускать повторного посещения уже пройденных узлов. Обычно это делается через аккумулятор списокпосещённыхвершинсписок посещённых вершинсписокпосещённыхвершин или через табличную дедукцию tabling/memoizationtabling / memoizationtabling/memoization.
Пример с аккумулятором стандартныйприём«visited»стандартный приём «visited»стандартныйприём«visited»:
% Внешняя обёртка, добавляем в список visited начальную вершину X ancestorX,YX,YX,Y :- ancestorX,Y,[X]X,Y,[X]X,Y,[X].
% базовый случай ancestor(X,Y,_) :- parentX,YX,YX,Y.
% рекурсивный случай: берем Z, который ещё не был посещён ancestorX,Y,VisitedX,Y,VisitedX,Y,Visited :- parentX,ZX,ZX,Z, + memberZ,VisitedZ,VisitedZ,Visited, % избегаем возвращения в уже посещённые ancestorZ,Y,[Z∣Visited]Z,Y,[Z|Visited]Z,Y,[Z∣Visited].
Этот вариант гарантирует, что при конечном числе вершин поиска не будет бесконечного повторного захода по циклу, потому что каждый узел может быть добавлен в Visited лишь однажды.
Другие способы:
Tabling memoizationmemoizationmemoization: в современных реализациях Prolog напримерSWI−Prologнапример SWI-PrologнапримерSWI−Prolog можно включить табличное вычисление, и тогда рекурсивные запросы в конечном графе гарантированно завершаются: :- table ancestor/2. оставляетеопределениеancestorкакувас—Prologавтоматическизапоминаетпромежуточныеответыоставляете определение ancestor как у вас — Prolog автоматически запоминает промежуточные ответыоставляетеопределениеancestorкакувас—Prologавтоматическизапоминаетпромежуточныеответы.Использовать итеративное углубление iterativedeepeningiterative deepeningiterativedeepening или алгоритмы поиска в ширину — они избегают проблем глубинного поиска, но требуют явной реализации.Можно применять cut !!! и дополнительные ограничения, но некорректно поставленный cut может скрыть ответы или не устранить зацикливание — поэтому предпочтительнее visited или tabling.
Короткий итог:
Prolog выполняет глубинный левосторонний поиск и использует backtracking для перебора вариантов.Циклы в фактах могут приводить к бесконечной рекурсии при глубинном поиске.Решения: хранить список посещённых узлов аккумулятораккумулятораккумулятор или включить tabling в вашей реализации Prolog.
Код, который вы привели:
parentalice,bobalice, bobalice,bob.
parentbob,carolbob, carolbob,carol.
ancestorX,YX,YX,Y :- parentX,YX,YX,Y.
ancestorX,YX,YX,Y :- parentX,ZX,ZX,Z, ancestorZ,YZ,YZ,Y.
что происходит при поиске и почему может возникнуть зацикливание:
1) Как Prolog ищет ответы SLD,левостороннийпоиск,глубинныйобходSLD, левосторонний поиск, глубинный обходSLD,левостороннийпоиск,глубинныйобход
Prolog пытается доказать цель запросзапросзапрос по правилам сверху вниз и слева направо.Для цели ancestorA,BA,BA,B он сначала подставляет первую клаузу ancestorX,YX,YX,Y :- parentX,YX,YX,Y. — это простой факт: он проверяет, есть ли parentA,BA,BA,B в базе фактов.Если первый путь не дал ответа, он пробует вторую клаузу ancestorX,YX,YX,Y :- parentX,ZX,ZX,Z, ancestorZ,YZ,YZ,Y. — сначала ищет parentA,ZA,ZA,Z берёткакое‑тоZберёт какое‑то Zберёткакое‑тоZ, затем рекурсивно пытается доказать ancestorZ,BZ,BZ,B.При неудаче какой‑то альтернативы Prolog делает backtracking: отменяет последние привязки переменных и пробует следующую альтернативную ветку следующийфактparent/2,следующийспособдоказатьпобочнуюцельилиследующуюклаузуancestor/2следующий факт parent/2, следующий способ доказать побочную цель или следующую клаузу ancestor/2следующийфактparent/2,следующийспособдоказатьпобочнуюцельилиследующуюклаузуancestor/2.Пример трассировки для запроса ancestoralice,carolalice, carolalice,carol:
Проверяется parentalice,carolalice, carolalice,carol — нет.Переходим ко второй клаузе: ищем parentalice,Zalice, Zalice,Z — находим Z = bob.Теперь доказать ancestorbob,carolbob, carolbob,carol:Проверяем parentbob,carolbob, carolbob,carol — да, успех. Значит ancestoralice,carolalice, carolalice,carol — успех.При дальнейшем backtracking Prolog может искать дополнительные Z для parentalice,Zalice, Zalice,Z и т.д.
2) Что такое backtracking
Backtracking — механизм отката привязок переменных и перебора альтернативных вариантов доказательства, когда текущая ветка не приводит к успеху или когда нужно найти дополнительные ответы. Prolog запоминает точку выбора choicepointchoice pointchoicepoint и при неудаче возвращается к ней, пробуя следующую возможность.3) Почему возникает бесконечная рекурсия и как её избежать
Алгоритм Prolog реализует глубинный поиск. При наличии циклов в графе отношений parent напримерparent(a,b).parent(b,a).например parent(a,b). parent(b,a).напримерparent(a,b).parent(b,a). и если искомая цель недостижима, глубинный рекурсивный переход может зациклиться: ancestora,Targeta,Targeta,Target → parenta,ba,ba,b → ancestorb,Targetb,Targetb,Target → parentb,ab,ab,a → ancestora,Targeta,Targeta,Target → ...Чтобы избежать зацикливания, нужно не допускать повторного посещения уже пройденных узлов. Обычно это делается через аккумулятор списокпосещённыхвершинсписок посещённых вершинсписокпосещённыхвершин или через табличную дедукцию tabling/memoizationtabling / memoizationtabling/memoization.Пример с аккумулятором стандартныйприём«visited»стандартный приём «visited»стандартныйприём«visited»:
% Внешняя обёртка, добавляем в список visited начальную вершину X
ancestorX,YX,YX,Y :- ancestorX,Y,[X]X,Y,[X]X,Y,[X].
% базовый случай
ancestor(X,Y,_) :- parentX,YX,YX,Y.
% рекурсивный случай: берем Z, который ещё не был посещён
ancestorX,Y,VisitedX,Y,VisitedX,Y,Visited :-
parentX,ZX,ZX,Z,
+ memberZ,VisitedZ,VisitedZ,Visited, % избегаем возвращения в уже посещённые
ancestorZ,Y,[Z∣Visited]Z,Y,[Z|Visited]Z,Y,[Z∣Visited].
Этот вариант гарантирует, что при конечном числе вершин поиска не будет бесконечного повторного захода по циклу, потому что каждый узел может быть добавлен в Visited лишь однажды.
Другие способы:
Tabling memoizationmemoizationmemoization: в современных реализациях Prolog напримерSWI−Prologнапример SWI-PrologнапримерSWI−Prolog можно включить табличное вычисление, и тогда рекурсивные запросы в конечном графе гарантированно завершаются::- table ancestor/2.
оставляетеопределениеancestorкакувас—Prologавтоматическизапоминаетпромежуточныеответыоставляете определение ancestor как у вас — Prolog автоматически запоминает промежуточные ответыоставляетеопределениеancestorкакувас—Prologавтоматическизапоминаетпромежуточныеответы.Использовать итеративное углубление iterativedeepeningiterative deepeningiterativedeepening или алгоритмы поиска в ширину — они избегают проблем глубинного поиска, но требуют явной реализации.Можно применять cut !!! и дополнительные ограничения, но некорректно поставленный cut может скрыть ответы или не устранить зацикливание — поэтому предпочтительнее visited или tabling.
Короткий итог:
Prolog выполняет глубинный левосторонний поиск и использует backtracking для перебора вариантов.Циклы в фактах могут приводить к бесконечной рекурсии при глубинном поиске.Решения: хранить список посещённых узлов аккумулятораккумулятораккумулятор или включить tabling в вашей реализации Prolog.