Why this dynamic version of Fibonacci program is incredibly faster then this other? Prolog solutions

北城余情 提交于 2021-01-27 16:11:34

问题


I am learning Prolog using SWI Prolog and I have a doubt about the followings two solutions of the Fibonacci number calculation program:

The first one is this:

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. 

It is pretty clear for me hw it work, it is very simple.

Then I have this second version that, reading the code, seems to work as the previous one but after that it have calculate the Fibonacci number of N save it in the Prolog database by asserta/2 predicate to reuse it after.

So for example if I calculate the Fibonacci number for 10 and for 11 when I go to calculate the Fibonacci number for 12 I will aspect that it use the result of the previous 2 computations.

So my code is:

:-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)).

It seems to me that the logic is the same of the previous one:

F is the Fibonacci number of N if N>2 and use the recursion for calculate the Fibonacci number of N (as in the preious example)

I expect that the program is faster if I ask to calculate the number of a Fibonacci number for a number that I have already calculated and put into the database Fibonacci numbers of its predecessors (or some of them) but seems to me that it work in a strange way: it is too fast and is able to directly calculate the numbers of Fibonacci for very large integers (the other version goes wrong with such large numbers)

The other strange thing is that if I do a trace of a query I obtain something like this:

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

As you can see it seems that don't execute the code of the Fibonacci predicate but directly obtain the result (from where?!?!)

Instad if I execute this query (using the first version), I obtain that the program will calculate it:

[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 .

Why? I would expect that the second version (the one that use asserta predicate) will calculate the Fibonacci number for two number and the it use these value for generate the solution of the following one.

For example I could have the following situation: I have never yet calculate any Fibonacci number and I ask for calculate the Fibonacci number of N=4 so it calculate it (as in the second posted stacktrace).

So I ask to calculate the Fibonacci number of N=5 and he use the Fibonacci of N=4 that it was saved. So then I ask it to calculate the Fibonacci number of N=6 and it finally could use the saved Fibonacci number of 4 and of 5

What am I missing? Can you help me to understand?


回答1:


TL;DR: use retractall to erase all previously asserted facts from memory.

Change your definition to

:- 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):-!) ).

Notice the cut inside the asserted rule. Also notice the retractall statement. Without it, all the previously asserted facts will remain in memory even if you reload the program. That's probably the reason why you were getting your results immediately.

After you've run e.g. ?- fibDyn(10,X) once, you can see all the asserted facts in the database:

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.

That is why it runs so fast. The difference in speed you're seeing is the difference between an exponential and linear time complexity algorithm.

Next time you call it, it has access to all the previously calculated results:

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

[trace] 16 ?- 

This explains your fibDyn(200,X) call trace output. You've probably tried it after you've already calculated it once or twice before.

Here's what happens when I next request the 11th number:

[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 ?- 

and again:

[trace] 36 ?- fibDyn(11,X).
   Call: (6) fibDyn(11, _G1068) ? creep
   Exit: (6) fibDyn(11, 89) ? creep
X = 89.



回答2:


Your first solution

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
  . 

isn't very efficient. For starter's it's not tail-recursive and it runs in exponential time (as noted earlier). I'm willing to bet that this recursive implementation, which should run in linear time, will be at least as fast (if not faster) than your dynamic solution:

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 )
  .

The important thing to note is that the Fibonacci sequence only needs to track the previous two elements in the series. Why recompute each of them on each iteration? Just keep a sliding window as above.

One of the more interesting properties of the Fibonacci sequence is that as you move out further into the sequence, the ratio of any two adjacent values more and more closely approximates phi, the Golden Mean. Even more interesting is that this holds true regardless of what two values are used to seed the sequence, so long as they are non-negative and at least one of them is zero.

A more generic solution that lets you seed the sequence with whatever values you want might be something like this:

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 )
  .



回答3:


This is not really about Prolog, but about algorithms. The naive recursive solution takes O(2**n) steps to compute, while the second version uses memoization to cut this down to O(n).

To see what this means, try calculating fib(4) on paper, without ever looking up things you calculated previously. Then, do it again but keep notes and look up whatever you can.

After that, if you try to calculate fib(5) the first way, you first have to calculate fib(4) and fib(3). That's what your first algorithm does. Note that in order to calculate fib(4) you need to calculate fib(3) again. So you end up doing the same calculation over and over again.

On the other hand, you could just look both of these values up and get the result immediately. That's what your second algorithm does.

With O(2**n), you need twice the amount of work for every subsequent value, while with O(n) you only need to do as much work as for the previous one.



来源:https://stackoverflow.com/questions/16358747/why-this-dynamic-version-of-fibonacci-program-is-incredibly-faster-then-this-oth

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!