Почему эта динамическая версия программы Фибоначчи невероятно быстрее, чем другая? Пролог-решения

Я изучаю Prolog с помощью SWI Prolog, и у меня есть сомнения относительно следующих двух решений программы расчета чисел Фибоначчи:

Первый такой:

fib(1,1).   
fib(2,1).   


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

Для меня довольно ясно, как это работает, это очень просто.

Затем у меня есть эта вторая версия, которая, читая код, кажется, работает как предыдущая, но после этого она вычисляет число Фибоначчи N, сохраняет его в базе данных Prolog с помощью предиката asserta/2 для повторного использования. это после.

Так, например, если я вычисляю число Фибоначчи для 10 и 11, когда я иду вычислять число Фибоначчи для 12, я буду использовать результат двух предыдущих вычислений.

Итак, мой код:

:-dynamic fibDyn/2.

fibDyn(1,1).
fibDyn(2,1).

fibDyn(N,F) :- N > 2,
               N1 is N-1,
               fibDyn(N1,F1),
               N2 is N-2,
               fibDyn(N2,F2),
               F is F1+F2,
               asserta(fibDyn(N,F)).

Мне кажется, что логика та же, что и в предыдущем:

F — число Фибоначчи для N, если N>2, и используйте рекурсию для вычисления числа Фибоначчи для N (как в предыдущем примере).

Я ожидаю, что программа будет работать быстрее, если я попрошу вычислить номер числа Фибоначчи для числа, которое я уже вычислил и занес в базу данных чисел Фибоначчи своих предшественников (или некоторых из них), но мне кажется, что она работает в странный способ: он слишком быстр и способен напрямую вычислять числа Фибоначчи для очень больших целых чисел (другая версия не работает с такими большими числами)

Другая странная вещь заключается в том, что если я делаю трассировку запроса, я получаю что-то вроде этого:

[trace]  ?- fibDyn(200,Fib).
   Call: (6) fibDyn(200, _G1158) ? creep
   Exit: (6) fibDyn(200, 280571172992510140037611932413038677189525) ? creep
Fib = 280571172992510140037611932413038677189525 .

Как видите, получается не выполнять код предиката Фибоначчи, а напрямую получать результат (откуда?!?!)

Вместо этого, если я выполняю этот запрос (используя первую версию), я получаю, что программа его вычислит:

[trace]  ?- fib(3,Fib).
   Call: (6) fib(3, _G1158) ? creep
^  Call: (7) 3>2 ? creep
^  Exit: (7) 3>2 ? creep
^  Call: (7) _G1233 is 3+ -1 ? creep
^  Exit: (7) 2 is 3+ -1 ? creep
   Call: (7) fib(2, _G1231) ? creep
   Exit: (7) fib(2, 1) ? creep
^  Call: (7) _G1236 is 3+ -2 ? creep
^  Exit: (7) 1 is 3+ -2 ? creep
   Call: (7) fib(1, _G1234) ? creep
   Exit: (7) fib(1, 1) ? creep
^  Call: (7) _G1158 is 1+1 ? creep
^  Exit: (7) 2 is 1+1 ? creep
   Exit: (6) fib(3, 2) ? creep
Fib = 2 .

Почему? Я ожидаю, что вторая версия (та, которая использует предикат asserta) будет вычислять число Фибоначчи для двух чисел и использовать это значение для генерации решения следующего.

Например, у меня может быть следующая ситуация: я еще никогда не вычислял числа Фибоначчи и прошу вычислить число Фибоначчи N=4, чтобы оно вычислило его (как во второй опубликованной трассировке стека).

Поэтому я прошу рассчитать число Фибоначчи для N=5, а он использует число Фибоначчи для N=4, чтобы оно было сохранено. Затем я прошу его рассчитать число Фибоначчи N = 6, и, наконец, он сможет использовать сохраненные числа Фибоначчи 4 и 5.

Что мне не хватает? Можете ли вы помочь мне понять?


person AndreaNobili    schedule 03.05.2013    source источник


Ответы (3)


TL;DR: используйте retractall, чтобы стереть из памяти все ранее утвержденные факты.

Измените свое определение на

:- dynamic fibDyn/2.
:- retractall( fibDyn(_,_) ).  %% without this, you'll retain all the previous 
                               %% facts even if you reload the program
fibDyn(1,1).
fibDyn(2,1).

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

Обратите внимание на разрез внутри утвержденного правила. Также обратите внимание на оператор retractall. Без него все ранее утвержденные факты останутся в памяти, даже если вы перезагрузите программу. Это вероятно, причина того, почему вы сразу получали результаты.

После того, как вы запустили, например. ?- fibDyn(10,X) один раз можно посмотреть все заявленные факты в базе:

12 ?- listing(fibDyn).
:- dynamic fibDyn/2.

fibDyn(10, 55) :- !.
fibDyn(9, 34) :- !.
fibDyn(8, 21) :- !.
fibDyn(7, 13) :- !.
fibDyn(6, 8) :- !.
fibDyn(5, 5) :- !.
fibDyn(4, 3) :- !.
fibDyn(3, 2) :- !.
fibDyn(1, 1).
fibDyn(2, 1).
fibDyn(A, D) :-
        A>2,
        B is A+ -1,
        fibDyn(B, E),
        C is A+ -2,
        fibDyn(C, F),
        D is E+F,
        asserta((fibDyn(A, D):-!)).

true.

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

В следующий раз, когда вы вызовете его, он получит доступ ко всем ранее рассчитанным результатам:

[trace] 15 ?- fibDyn(10,X).
   Call: (6) fibDyn(10, _G1068) ? creep
   Exit: (6) fibDyn(10, 55) ? creep
X = 55.

[trace] 16 ?- 

Это объясняет ваши выходные данные трассировки вызовов fibDyn(200,X). Вы, вероятно, пробовали это после того, как уже вычислили его один или два раза раньше.

Вот что происходит, когда я в следующий раз запрашиваю 11-й номер:

[trace] 35 ?- fibDyn(11,X).
   Call: (6) fibDyn(11, _G1068) ? creep
   Call: (7) 11>2 ? creep
   Exit: (7) 11>2 ? creep
   Call: (7) _G1143 is 11+ -1 ? creep
   Exit: (7) 10 is 11+ -1 ? creep
   Call: (7) fibDyn(10, _G1144) ? creep
   Exit: (7) fibDyn(10, 55) ? creep
   Call: (7) _G1146 is 11+ -2 ? creep
   Exit: (7) 9 is 11+ -2 ? creep
   Call: (7) fibDyn(9, _G1147) ? creep
   Exit: (7) fibDyn(9, 34) ? creep
   Call: (7) _G1068 is 55+34 ? creep
   Exit: (7) 89 is 55+34 ? creep
^  Call: (7) asserta((fibDyn(11, 89):-!)) ? creep
^  Exit: (7) asserta((fibDyn(11, 89):-!)) ? creep
   Exit: (6) fibDyn(11, 89) ? creep
X = 89.

[trace] 36 ?- 

и опять:

[trace] 36 ?- fibDyn(11,X).
   Call: (6) fibDyn(11, _G1068) ? creep
   Exit: (6) fibDyn(11, 89) ? creep
X = 89.
person Will Ness    schedule 03.05.2013
comment
ну так мне понятно!!! Tnx так много. У меня есть последний вопрос по этому поводу, пожалуйста, подтвердите или отклоните. В динамическом случае (в котором я использую assert для сохранения результата) имеет ли вычислительное дерево ту же высоту, что и вычислительное дерево в нединамической программе? Я думаю, да, потому что, однако, нужно вычислить все решение, но я думаю, что после вычисления всего решения в более длинной ветви дерева многие другие ветви, соответствующие уже рассчитанным решениям, обрезаются (в том смысле, что решение не пересчитывается) . Верна ли моя интерпретация? Tnx - person AndreaNobili; 03.05.2013
comment
@AndreaNobili о праве. Потому что мы добавляем заранее рассчитанные факты, а с нарезкой нет поиска там, где в незапоминаемой версии мы бы его выполняли, заново, для меньшего числа. Высота дерева одинаковая только при самом первом прогоне, потому что при следующих прогонах (с тем же аргументом) высота равна 1 - факт уже есть. Незапоминаемая версия входит в оба маршрута, левый (N-1) и правый (N-2), поэтому обход повторяется много раз. Запоминаемая версия спускается по высоте только один раз. Разница огромна - между ~ 1.6^n и n. - person Will Ness; 03.05.2013
comment
@WillNess, можете ли вы проверить мой вопрос здесь stackoverflow.com/questions/50622088/ этот способ тоже очень быстрый - person vipin; 31.05.2018

Ваше первое решение

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

не очень эффективен. Для начала это не хвостовая рекурсия и работает в экспоненциальном времени (как отмечалось ранее). Готов поспорить, что эта рекурсивная реализация, которая должна выполняться за линейное время, будет как минимум такой же быстрой (если не быстрее), как ваше динамическое решение:

fibonacci( 1 , 1 ) .
fibonacci( 2 , 1 ) .
fibonacci( N , V ) :- N>2, fibonacci( 1 , 1 , 3 , N , V ) .

fibonacci( X , Y , N , N , V ) :-
  V is X+Y
  .
fibonacci( X , Y , T , N , V ) :-
  Z  is X + Y ,
  T1 is T + 1 ,
  fibonacci( Y , Z , T1 , N , V )
  .

Важно отметить, что последовательность Фибоначчи должна отслеживать только два предыдущих элемента в ряду. Зачем пересчитывать каждый из них на каждой итерации? Просто сохраните скользящее окно, как указано выше.

Одно из наиболее интересных свойств последовательности Фибоначчи заключается в том, что по мере того, как вы продвигаетесь дальше по последовательности, отношение любых двух соседних значений все больше и больше приближается к phi, золотому сечению. Еще более интересно то, что это верно независимо от того, какие два значения используются для заполнения последовательности, если они неотрицательны и хотя бы одно из них равно нулю.

Более общее решение, позволяющее заполнять последовательность любыми значениями, может выглядеть примерно так:

fibonacci( Seed1 , Seed2 , Limit , N , Value ) :-
  Seed1 >= 0       ,
  Seed2 >= 0       ,
  X is Seed1+Seed2 ,
  X > 0            ,
  Limit >= 0        ,
  fibonacci( Seed1 , Seed2 , 3 , Limit , N , Value )
  .

fibonacci( S1 , _  , _ , L , 1 , S1 ) :- 1 =< L .
fibonacci( _  , S2 , _ , L , 2 , S2 ) :- 2 =< L .
fibonacci( S1 , S2 , T , L , T , V  ) :-          % T > 2,
  T =< L ,
  V is S1+S2
  .
fibonacci( S1 , S2 , T , L , N , V ) :- N > T,    % T > 2,
  T =< L        ,
  S3 is S1 + S2 ,
  T1 is T  + 1  ,
  fibonnaci( S2 , S3 , T1 , L , N , V )
  .
person Nicholas Carey    schedule 03.05.2013

Дело не в Прологе, а в алгоритмах. Наивное рекурсивное решение требует O(2**n) шагов для вычисления, в то время как вторая версия использует мемоизацию, чтобы сократить это до O(n).

Чтобы понять, что это значит, попробуйте вычислить fib(4) на бумаге, даже не обращаясь к ранее рассчитанным значениям. Затем сделайте это снова, но делайте заметки и ищите все, что сможете.

После этого, если вы попытаетесь вычислить fib(5) первым способом, вам сначала придется вычислить fib(4) и fib(3). Это то, что делает ваш первый алгоритм. Обратите внимание, что для вычисления fib(4) вам нужно снова вычислить fib(3). Таким образом, вы в конечном итоге делаете один и тот же расчет снова и снова.

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

С O(2**n) вам нужно вдвое больше работы для каждого последующего значения, а с O(n) вам нужно сделать столько же работы, сколько и для предыдущего.

person firefrorefiddle    schedule 03.05.2013
comment
нет, это это о Прологе (ИМХО, то есть :) ). ОП забыл включить оператор retractall в свой файл и получил двухэтапную трассировку (также при самом первом вызове, после перезагрузки) вместо C-шагов (C какая-то константа, видимо ~16). Но ваше объяснение алгоритмов, конечно, совершенно справедливо. :) - person Will Ness; 03.05.2013