Пролог, Dynami c Программирование, серия Фибоначчи - PullRequest
1 голос
/ 04 апреля 2020

Я должен предварить это, сказав, что это проблема с домашней работой, с которой у меня возникают проблемы, и я не уверен, разрешено ли подобное здесь, но я не знаю, куда еще обратиться. Вот вопрос, который мне задавали:

В примере кода для этого вопроса вы можете увидеть предикат Фибоначчи fibSimple/2, который вычисляет Фибоначчи от X, натуральное число. Проблема с наивным рекурсивным решением состоит в том, что в итоге вы пересчитываете один и тот же рекурсивный случай несколько раз. См. Здесь для объяснения.

Например, разработка fib (5) включает в себя разработку решения для fib (2) три раза. Dynami c Программный подход может решить эту проблему. По сути, все сводится к тому, чтобы начать с fib (2), затем вычислить fib (3), затем fib (4) et c .... до достижения fib (X). Вы можете хранить эти ответы в списке, причем fib (X) будет первым элементом списка.

Ваши базовые варианты будут выглядеть следующим образом:

fib(0,[0]).
fib(1,[1,0]).

Обратите внимание, что fib (1) определяется как [1,0]. fib (1) действительно равен 1, но мы ведем список предыдущих ответов.

Почему мы это делаем? Потому что для вычисления fib (X) нам просто нужно вычислить fib (X-1), сложить вместе первые два элемента и вставить их в начало списка. Например, из вышесказанного легко вычислить fib (2, Ans). FIB (2) в этом случае будет [1,1,0]. Тогда fib (3) будет [2,1,1,0], fib (4) будет [3,2,1,1,0] et c ....

Завершите Предикат fib / 2, как указано выше, базовые случаи показаны выше. Вам нужно выяснить одну строку, которая идет после базовых вариантов для обработки рекурсии.

Это пример кода, который они предоставили

fibSimple(0,0). % fib of 0 is 0
fibSimple(1,1). % fib of 1 is 1
fibSimple(N,X) :- N>1,fibSimple(N-1,A), fibSimple(N-2,B), X is A+B.

fib(0,[0]).
fib(1,[1,0]).

У меня было несколько попыток сделать это, и хотя я совершенно уверен, что моя попытка в конечном итоге окажется безнадежно ошибочной, это то, что я недавно пытался

fib(X,[fib(X-2)+fib(X-1) | _]).

Я рассуждаю так: если вы сможете получить ответ последние 2, и сложите их вместе, сделав их первым или «заголовком» списка, а затем подчеркиванием, представляющим остальные.

Мои 2 вопроса:

1) Я не знаю / думаю, что это подчеркивание сделает то, что я хочу, и я потерялся в том, куда go отсюда и

2) Я не знаю, как даже запустить эту программу, так как предикат fib\2 требует 2 параметры. И скажем, например, что я хотел запустить fib\2, чтобы найти число Фибоначчи от 5, я бы не знал, что ввести в качестве второго параметра.

Ответы [ 2 ]

2 голосов
/ 04 апреля 2020

Поскольку это домашнее задание, я лишь нарисую решение, но оно должно ответить на заданные вами вопросы.

Предикат отличается от функции тем, что у него нет возвращаемого значения. Пролог просто говорит вам, может ли он вывести его (*). Поэтому, если вы просто спросите, истинно ли fib(5), лучшее, что вы можете получить, - это «да». Но каковы тогда числа Фибоначчи от 1 до 5? Вот тут и вступает второй аргумент. Либо вы уже знаете и проверяете:

?- fib(5, [5, 3, 2, 1, 1, 0]).
true ;                   <--- Prolog can derive this fact. With ; I see more solutions.
false                    <--- no, there are no other solutions

Или вы оставляете второй аргумент как переменную, и Prolog сообщит вам, какие значения должна иметь эта переменная, чтобы она могла вывести ваш query:

?- fib(5, X).
X = [5, 3, 2, 1, 1, 0] ;
false.

Таким образом, второй аргумент содержит результат, который вы ищете.

Вы также можете задать другие запросы, например fib(X,Y) ", какие числа и их хосты Фибоначчи мы можем получить ?» или fib(X, [3 | _]) «какое число вычисляет число Фибоначчи 3?». Во втором случае мы использовали подчеркивание, чтобы сказать, что остальная часть списка не имеет значения. (2)

Так что же нам делать с fib(X,[fib(X-2)+fib(X-1) | _]).? Если мы добавим его к предложениям для 0 и 1, которые вам дали, мы можем просто запросить все результаты:

?- fib(X,Y).
X = 0,
Y = [1] ;    <-- first solution X = 0, Y = [1]
X = 1,
Y = [1, 0] ; <-- second solution X = 1, Y = [1, 0]
Y = [fib(X-2)+fib(X-1)|_2088]. <-- third solution

Третье решение просто говорит: список, начинающийся с термина fib(X-2)+fib(X-1), является допустимым решением. (_2088 как просто переменная, которая не была названа вами). Но, как упоминалось в начале, этот термин не оценивается. Вы могли бы получить аналогичные результаты, определив fib(X, [quetzovercaotl(X-1) | _]).

Так похоже на fibSimple, что вам нужно правило, которое говорит Прологу, как выводить новые факты из уже известных им фактов. Я переформатировал fibSimple для вас:

fibSimple(N,X) :-
  N>1,
  fibSimple(N-1,A),
  fibSimple(N-2,B),
  X is A+B.

Это говорит, что если N > 1, и мы можем получить fibSimple(N-1,A), и мы можем получить fibSimple(N-2,B), и мы можем установить X в результат A + B , тогда мы получим fibSimple(N, X). Отличие от того, что вы написали, заключается в том, что fibSimple(N-1,A) встречается в теле правила. Снова аргумент N-1 не оценивается. На самом деле происходит то, что рекурсия создает термины 3-1 и (3-1)-1) при вызове с запросом fib(3,X). Фактическая оценка происходит в арифметических c предикатах is и <. Например, рекурсивный предикат останавливается, когда он пытается оценить (3-1)-1 > 1, поскольку 1>1 не соответствует действительности. Но мы также не затрагиваем базовый вариант fibSimple(1, 1), поскольку термин (3-1)-1 не совпадает с 1, даже если они оцениваются на одно и то же число.

Именно поэтому Prolog не находит число Фибоначчи 3 в простой реализации:

?- fibSimple(3, X).
false.

Арифметическое c вычисление выполняется предикатом is: запрос X is (3-1) -1 имеет именно такое решение X = 1. (3)

Таким образом, fibSimple должен действительно выглядеть следующим образом: (4)

fibSimple(0,1).
fibSimple(1,1).
fibSimple(N,X) :-
    N>1,
    M1 is N -1,      % evaluate N - 1
    M2 is N -2,      % evaluate N - 2
    fibSimple(M1,A),
    fibSimple(M2,B),
    X is A+B.

Для fib вы можете использовать это как шаблон, где вам нужен только один рекурсивный вызов, потому что A и B находятся в списке истории. Будьте осторожны с заголовком вашего предложения: если X является новым значением, оно также не может быть новым списком истории. Например, голова может иметь форму fib(N, [X | Oldhistory]).

Удачи с домашним заданием!

(1) Это немного упрощено - Пролог обычно дает вам замену ответа, которая говорит Вы, какие значения переменных в вашем запросе. Есть также несколько ограниченных способов справиться с не выводимостью, но здесь вам это не нужно.

(2) Если вы используете предикаты арифметики c is и >, эти два запроса будут не работать с простой реализацией. Более декларативный способ справиться с этим - арифметика c ограничения .

(3) Чтобы эта оценка работала, правая часть is может не содержать переменных. Здесь вам понадобятся арифметические c ограничения из (2).

(4) В качестве альтернативы, базовые случаи могут оценивать арифметические c слагаемые, которые были переданы:

fibSimple(X, 0) :-
    0 is X.
fibSimple(X, 1) :-
    1 is X.
fibSimple(N,X) :-
    N>1,
    fibSimple(N-1,A),
    fibSimple(N-2,B),
    X is A+B.

Но это менее эффективно, потому что одно число занимает гораздо меньше места, чем термин 100000 - 1 - 1 -1 .... -1.

0 голосов
/ 04 апреля 2020

Чтобы продолжить работу с 2), вам придется прочитать вводную работу для Пролога, например Learn Prolog Now .

Но давайте ответим на вопрос о вычислении ряда Фибоначчи с помощью программы Prolog (которая является своего рода «программой logi c», но на самом деле просто вычисляет вперед), которая обменивает память на циклы ЦП

Запись в Википедии о Dynami c Программирование фактически содержит секцию о Последовательности Фибоначчи .

Как правильно сказано, проблема вычисления последовательности состоит из двух Особенности:

  • Рекурсивный
  • «Перекрывающиеся подзадачи»

Второй означает, что нас попросят пересчитать то же самое f (х) снова и снова. Таким образом, мы находимся в ситуации, когда мы можем обменять это повторяющееся усилие на пространство (то есть на память), а именно путем кэширования результатов вычисления f (x) . f должна быть реальной функцией: одни и те же аргументы всегда дают одинаковые результаты. Никаких запросов пользователю или чему-либо еще!

Использование "табуляции"

На самом деле, многие процессоры (движки) Prolog уже имеют встроенную возможность для включения кэширования для определенных предикатов: он называется Включение или «Мемоизация» и его обобщение на удивление сложны. Здесь - это страница из SWI-Prolog (я использую SWI-Prolog, потому что это тот, который я обычно использую, но XSB Prolog и B-Prolog могут иметь более полные реализации, не зная, в каком состоянии сейчас находится ).

Непосредственно из пролога SWI выполните c, мы получим эту реализацию. Обратите внимание на директиву :- table fib/2 , которая включает кэширование результатов для fib/2:

:- table fib_tabled/2.

fib_tabled(0, 1) :- !.
fib_tabled(1, 1) :- !.
fib_tabled(N, F) :-
        N > 1,
        N1 is N-1,
        N2 is N-2,
        fib_tabled(N1, F1),
        fib_tabled(N2, F2),
        F is F1+F2.

Выполнение вышеуказанного эффективно!

Включение включено:

?- time(fib_tabled(1000,F)).
% 4 inferences, 0.000 CPU in 0.000 seconds (95% CPU, 197141 Lips)
F = 70330367711422815821835254877183549770181269836358732742604905087154537118196933579742249494562611733487750449241765991088186363265450223647106012053374121273867339111198139373125598767690091902245245323403501.

Включение отключено:

?- time(fib_tabled(1000,F)).
...uh forget it.

Обратите внимание, что fib/2 ведет себя как функция больше, чем предикат . Режим аннотации будет fib(+N,-F): N входит, F выходит! В SWI Prolog произвольные длинные числа поступают через GNU Multiple Precision Arithmeti c Library

Не используется табулирование

Но мы хотим найти решение, не полагаясь на табулирование ,

Не глядя на эту страницу вообще ...

У нас есть два способа сделать это:

  • Сверху вниз : Начиная вычисление fib (N) при N и рекурсивное разложение до достижения fib (0) и fib (1) . Здесь мы должны отложить вычисление более высоких значений до тех пор, пока более низкие значения не станут известны. Это звучит как «Dynami c Programming» и то, что мы хотим сделать.
  • Вверх : Начинается вычисление fib (N) в 0 и 1 (для которых известны результаты) и итеративно движется вверх до N . Здесь мы знаем все нижние значения fib (m) , 0 <= m <n, при любом <em>n , при этом двигаясь в направлении N . Эффективно, но не то, что мы хотим сделать. (код для этого также находится внизу этой страницы ).

Итак ... "сверху вниз" это так.

Подходы сверху вниз

Нам нужен кеш, в котором хранятся уже вычисленные результаты.

Подход 1

В Прологе нет «изменяемых структур данных» (в принципе), поэтому первый подход будет следующим: с учетом существующего кэша нам придется построить новый кеш из него, вставка недавно вычисленного значения и продолжение работы с этим новым кешем. Кеш становится «горячим картофелем», который передается от одного звонка к другому. Или это «пронизывает» вычисления. Он появляется в двух местах в каждом списке аргументов: входящий, выходящий.

% In  goes:  N and CacheIn, 
% Out comes: X and CacheOut
% as indicated by the (purely notational in bare Prolog) "mode flags"
% fib(+N, -X, +CacheIn, -CacheOut)

fib(N, X, CacheIn, CacheOut) :- ....

Обратите внимание, что кеш - это нечто, находящееся "вне проблемы": оно должно быть актуальным и доступным для всех вызовов fib в любое время в самом свежем виде версия. В императивном языке программирования это будет изменчивая структура, на которую ссылаются fib . Способ Prolog на самом деле также гарантирует, что каждый вызов fib имеет доступ к последнему кешу. (докажи это!).

(Но что, если мы действительно будем иметь дело с ... параллелизмом (в отличие от простого параллелизма)? Тогда проблемы становятся более масштабными, и необходима синхронизация при доступе к кешу. Мы не go Значительные усилия по детализированному параллелизму в Прологе были заброшены )

Итак, кеш может быть списком длины N + 1 , где мы храним значение fib (n) в индексе n (список индексов go из 0), если оно известно. Если это не известно, мы можем, например, сохранить там 0.

Альтернативный подход может быть основан на кеше, построенном на карте . Они доступны в SWI-Prolog напрямую как " dicts " и, конечно, в других прологах через библиотеки, но с другим синтаксисом (Dict еще не стандартизирован.).

Подход 2

Но тогда есть гораздо лучший подход . Вместо вставки 0 для неизвестного пока в кеш, мы можем вставить fre sh, неограниченные переменные. Переменная - это глобальные данные, которые являются версионными (повышенная версия в точках выбора, пониженная версия при обратном отслеживании), и ее можно сделать «более точной» или «уточненной» по мере того, как время прогрессирует, например, установить в целое число (и все, вы не можете "неточный").

После некоторых проб и ошибок мы находим решение, которое просто принимает Cache (а не CacheIn и CacheOut) и уточняет переменная fre sh в кеше в позиции от x до fib (x) . Это очень близко к обязательному подходу - иметь глобальный кеш, значения которого устанавливаются в ходе вычислений. Ключом является предикат var(X), который фактически проверяет, является ли X неопределенной переменной или чем-то еще (в данном случае, действительным целочисленным значением).

Подход 3

(Это вызывает еще одну идею, основанную не на видимом кеше, а на невидимом, управляемом Prolog на основе сопрограммирования и предиката freeze. Возможно, позже.)

В rosettacode.org , в разделе Фибоначчи / Пролог, дано довольно интересное решение. Он использует ленивые списки:

fib([0,1|X]) :-
     ffib(0,1,X).
     ffib(A,B,X) :-
     freeze(X, (C is A+B, X=[C|Y], ffib(B,C,Y)) ).

?-  fib(X), length(A,15), append(A,_,X), writeln(A).
[0,1,1,2,3,5,8,13,21,34,55,89,144,233,377]
X = [0, 1, 1, 2, 3, 5, 8, 13, 21|...],
A = [0, 1, 1, 2, 3, 5, 8, 13, 21|...],
freeze(_16710,  (_16840 is 233+377, _16710=[_16840|_16866], ffib(377, _16840, _16866))).

Это для дневного курса от Intro до Prolog. Позже.

Подход 2: Кэшировать как список переменных fre sh, глобально обновленных

Код

Хорошо, давайте начнем эту вечеринку.

Не думаю, что я нашел это с первой попытки. Если вы понимаете это, вы понимаете Пролог.


% The predicates visible to the user

fib_cached(0, 1) :- !.
fib_cached(1, 1) :- !.
fib_cached(N, X) :- 
   N>1,!,                     % useless guard and cut but good for documentation
   Np is N+1,
   length(Cache,Np),          % create a cache as a list of Np fresh variables
   nth0(0,Cache,1),           % looks like a "get", actually a "set": Cache[0] := 1
   nth0(1,Cache,1),           % looks like a "get", actually a "set": Cache[1] := 1
   nth0(N,Cache,X),           % just the "pick the value" out of Cache and unify it with the result; in essence this is a communnication channel
   fib_cached_sub(N, Cache).  % fib_cached_sub doesn't even need to tells us the value, we will just look at the cache
   % At this point, one could print: format("Cache is now: ~w\n",[Cache]).

% Under-the-surface predicate!

fib_cached_sub(N, Cache) :- 
   nth0(N,Cache,Q),    % Unify Cache[N] with Q (Q is fresh, so this is a question)
   var(Q),             % Q is still a fresh variable!
   !,                  % Commit to "we need to compute"
   N1 is N-1,
   N2 is N-2,        
   % Recursive calls! Calls don't indicate the result,
   % they refine the cache! Note we will have a cache hit for N1=0,1
   fib_cached_sub(N1, Cache), 
   fib_cached_sub(N2, Cache),
   % On return, get values from the cache
   nth0(N1,Cache,F1),         
   nth0(N2,Cache,F2),
   F is F1+F2,
   % A really interesting question is: 
   % Could cache contain an entry for fib(N) now?
   % If not, why not? :-)
   % Unify Cache[N] with F (F is a value, so this is a "assignment/refinment")        
   nth0(N,Cache,F).

% Default case of a cache hit. There is nothing to do.

fib_cached_sub(N, Cache) :- 
   nth0(N,Cache,Q),
   \+var(Q).

Но это быстро?

Это эффективно по времени? Не так много, как табличный предикат:

?- time(fib_cached(1000,F)).
% 1,009,009 inferences, 0.114 CPU in 0.115 seconds (99% CPU, 8854526 Lips)
F = 70330367711422815821835254877183549770181269836358732742604905087154537118196933579742249494562611733487750449241765991088186363265450223647106012053374121273867339111198139373125598767690091902245245323403501.

Тесты

Время для тестирования с использованием средств тестирования SWI-Prolog

:-begin_tests(fibonacci).

test(fib_compare) :- 
   foreach(
      between(0,100,N), fib_compare_results(N)).


fib_compare_results(N) :-      
   fib_tabled(N,Ft),
   format("fib_tabled(~w) = ~w, ",[N,Ft]),
   fib_cached(N,Fc),
   format("fib_cached(~w) = ~w\n",[N,Fc]),
   %format("N = ~d, F_tabled = ~d, F_cached = ~d\n",[N,Ft,Fc]),
   Ft=:=Fc.

:-end_tests(fibonacci).

Запуск этого :

?- run_tests(fibonacci).
% PL-Unit: fibonacci fib_tabled(0) = 1, fib_cached(0) = 1
fib_tabled(1) = 1, fib_cached(1) = 1
fib_tabled(2) = 2, fib_cached(2) = 2
....
fib_tabled(100) = 573147844013817084101, fib_cached(100) = 573147844013817084101
. done
% test passed
true.

Подход 1. Кэширование в виде списка значений, локально обновляемых, «поточных»

Код

% The predicates visible to the user

fib_cachemutator(0, 1) :- !.
fib_cachemutator(1, 1) :- !.
fib_cachemutator(N, X) :- 
   N>1,!,                                      % useless guard and cut but good for documentation
   Nm is N-1,
   findall(0,between(0,Nm,_),Ctmp),            % create a cache as a list of N-1 0s
   Cache = [1,1|Ctmp],                         % first two entries of cache are 1
   fib_cachemutator_sub(N, Cache, CacheOut),   % Cache goes in, different Cache comes out
   nth0(N,CacheOut,X).


% The under-the-surface predicates

% Cache miss

fib_cachemutator_sub(N, Cache, CacheOut) :- 
   nth0(N,Cache,0),    % there is still a 0 there   
   !,                  % commit to "we need to compute"
   N1 is N-1,
   N2 is N-2,        
   fib_cachemutator_sub(N1, Cache, CacheTmp), 
   fib_cachemutator_sub(N2, CacheTmp, CacheTmp2),
   nth0(N1,CacheTmp2,F1),
   nth0(N2,CacheTmp2,F2),   
   F is F1+F2,
   update_cache(N,F,CacheTmp2,CacheOut).

% Cache hit

fib_cachemutator_sub(N, Cache, Cache) :-
   nth0(N,Cache,F), 
   F > 0.

% Create a new cache ("Update the cache"). This is laborious.
% I want an equivalent of
% https://perldoc.perl.org/functions/splice.html

update_cache(N,V,CacheIn,CacheOut) :-
   split_list(CacheIn,N,_,Front,Back),
   append([Front, [V], Back], CacheOut).


% Split a list at position N (0-based):
% split_list(+List, +Index, Element, FrontOfListUpToIndex, BackOfListFromIndex)

split_list([L|Lr],N, El, [L|Front], Back) :- 
   N>0,!, 
   Nm is N-1, 
   split_list(Lr,Nm,El,Front,Back).

split_list([L|Lr],0, L,  [],        Lr).

Но это быстро?

Это эффективно по времени? Не так сильно, как табличный предикат:

?- time(fib_cachemutator(1000,F)).
% 2,860,996 inferences, 0.361 CPU in 0.363 seconds (99% CPU, 7930777 Lips)
F = 70330367711422815821835254877183549770181269836358732742604905087154537118196933579742249494562611733487750449241765991088186363265450223647106012053374121273867339111198139373125598767690091902245245323403501.

Тесты

Как обычно:

:-begin_tests(fibonacci_bis).

test(fib_compare) :- 
   foreach(
      between(0,100,N), fib_compare_results(N)).

fib_compare_results(N) :-      
   fib_tabled(N,Ft),
   format("fib_tabled(~w) = ~w, ",[N,Ft]),
   fib_cachemutator(N,Fc),
   format("fib_cachemutator(~w) = ~w\n",[N,Fc]),
   %format("N = ~d, F_tabled = ~d, F_cached = ~d\n",[N,Ft,Fc]),
   Ft=:=Fc.

:-end_tests(fibonacci_bis).

Запуск этого:

?- run_tests(fibonacci_bis).
% PL-Unit: fibonacci_bis fib_tabled(0) = 1, fib_cachemutator(0) = 1
fib_tabled(1) = 1, fib_cachemutator(1) = 1
fib_tabled(2) = 2, fib_cachemutator(2) = 2
fib_tabled(3) = 3, fib_cachemutator(3) = 3
fib_tabled(4) = 5, fib_cachemutator(4) = 5
....
fib_tabled(100) = 573147844013817084101, fib_cachemutator(100) = 573147844013817084101
. done
% test passed
true.
...