Prolog, Dynamic Programming, Fibonacci series

若如初见. 提交于 2020-04-16 05:47:22

问题


I should preface this by saying this is a homework problem that I am having issues with, and Im not sure if that sort of thing is allowed around here, but I dont know where else to turn to. This is the question I've been asked:

In the sample code for this question, you can see a Fibonacci predicate fibSimple/2 which calculates the Fibonacci of X, a natural number. The problem with the naive recursive solution, is that you end up recalculating the same recursive case several times. See here for an explanation.

For example, working out the fib(5) involves working out the solution for fib(2) three separate times. A Dynamic Programming approach can solve this problem. Essentially, it boils down to starting with fib(2), then calculating fib(3), then fib(4) etc.... until you reach fib(X). You can store these answers in a list, with fib(X) ending up as the first item in the list.

Your base cases would look like the following:

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

Note the way that fib(1) is defined as [1,0]. fib(1) is really 1 but we are keeping a list of previous answers.

Why do we do this? Because to calculate fib(X), we just have to calculate fib(X-1) and add the first two elements together and insert them at the front of the list. For example, from the above, it is easy to calculate fib(2,Ans). fib(2) in this case would be [1,1,0]. Then fib(3) would be [2,1,1,0], fib(4) would be [3,2,1,1,0] etc....

Complete the fib/2 predicate as outlined above - the base cases are shown above. You need to figure out the one line that goes after the base cases to handle the recursion.

This is the sample code they provided

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

I've had a few attempts at this, and while I'm fairly certain my attempt will end up being hopelessly wrong, this is what I have most recently tried

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

My reasoning to this is that if you can get the answer to the last 2, and add them together making them the first or "head" of the list, and then the underscore representing the rest.

My 2 issues are:

1) I don't know/think this underscore will do what I want it to do, and am lost in where to go from here and

2) I don't know how to even run this program as the fib\2 predicate requires 2 parameters. And lets say for example I wanted to run fib\2 to find the fibonacci of 5, I would not know what to put as the 2nd parameter.


回答1:


Because this is homework I will only sketch the solution - but it should answer the questions you asked.

A predicate differs from a function in that it has no return value. Prolog just tells you if it can derive it (*). So if you just ask if fib(5) is true the best you can get is "yes". But what are the Fibonacci numbers from 1 to 5 then? That's where the second argument comes in. Either you already know and check:

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

Or you leave the second argument as a variable and Prolog will tell you what values that variable must have such that it can derive your query:

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

So the second argument contains the result you are looking for.

You can also ask the other queries like fib(X,Y) "which numbers and their fibonacci hostories can we derive?" or fib(X, [3 | _]) "which number computes the the fibonacci number 3?". In the second case, we used the underscore to say that the rest of the list does not matter. (2)

So what do we do with fib(X,[fib(X-2)+fib(X-1) | _]).? If we add it to the clauses for 0 and 1 you were given we can just query all results:

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

The third solution just says: a list that begins with the term fib(X-2)+fib(X-1) is a valid solution (the _2088 as just a variable that was not named by you). But as mentioned in the beginning, this term is not evaluated. You would get similar results by defining fib(X, [quetzovercaotl(X-1) | _]).

So similar to fibSimple you need a rule that tells Prolog how to derive new facts from facts it already knows. I have reformatted fibSimple for you:

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

This says if N > 1 and we can derive fibSimple(N-1,A) and we can derive fibSimple(N-2,B) and we can set X to the result of A + B, then we derive fibSimple(N, X). The difference to what you wrote is that fibSimple(N-1,A) occurs in the body of the rule. Again the argument N-1 does not get evaluated. What actually happens is that the recursion constructs the terms 3-1 and (3-1)-1) when called with the query fib(3,X). The actual evaluation happens in the arithmetic predicates is and <. For example, the recursive predicate stops when it tries to evaluate (3-1)-1 > 1 because 1>1 is not true. But we also do not hit the base case fibSimple(1, 1) because the term (3-1)-1 is not the same as 1 even though they evaluate to the same number.

This is the reason why Prolog does not find the Fibonacci number of 3 in the simple implementation:

?- fibSimple(3, X).
false.

The arithmetic evaluation is done by the is predicate: the query X is (3-1) -1 has exactly the solution X = 1. (3)

So fibSimple must actually look like this: (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.

For fib you can use this as a template where you only need one recursive call because both A and B are in the history list. Be careful with the head of your clause: if X is the new value it can not also be the new history list. For example, the head could have the form fib(N, [X | Oldhistory]).

Good luck with the homework!

(1) This is a little simplified - Prolog will usually give you an answer substitution that tells you what values the variables in your query have. There are also some limited ways to deal with non-derivability but you don't need that here.

(2) If you use the arithmetic predicates is and > these two queries will not work with the straightforward implementation. The more declarative way of dealing with this is arithmetic constraints.

(3) For this evaluation to work, the right hand side of is may not contain variables. This is where you would need the arithmetic constraints from (2).

(4) Alternatively, the base cases could evaluate the arithmetic terms that were passed down:

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.

But this is less efficient because a single number takes much less space than the term 100000 - 1 - 1 -1 .... -1.




回答2:


To get ahead with 2) you will have to read an introductory work to Prolog, like Learn Prolog Now.

But let's answer the question about computing the Fibonacci series with a Prolog program (which is sort of a "logic program", but actually just computes forwards) that trades memory against CPU cycles

The Wikipedia entry on Dynamic Programming has actually a section on the Fibonacci Sequence.

As correctly said, the problem of computing the sequence has two features:

  • Recursive
  • "Overlapping sub-problems"

The second one means that we will be asked to recompute the same f(x) again and again. We are thus in a situation where we can trade this repeated effort against space (i.e. memory), namely by caching the results of computing f(x). f must be a real function of course: Same arguments always yield same results. No querying the user or anything!

Using "tabling"

In fact, many Prolog Processors (engines) already have a built-in facility to switch caching on for certain predicates: It's called Tabling or "Memoization" and actually generalizing it is surprisingly complex. Here is the page from SWI-Prolog (I'm using SWI-Prolog because that's the one I traditionally use, but XSB Prolog and B-Prolog may have more complete implementations, not sure what the state is now).

Directly from the SWI Prolog doc, we get this implementation. Note the :- table fib/2 directive, which switches on caching of results for 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.

Execution of the above is efficient!

Tabling is on:

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

Tabling is off:

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

Note that the fib/2 behaves like a function more than a predicate. The mode annotations would be fib(+N,-F): N goes in, F comes out! In SWI Prolog, arbitrary long numbers come via the GNU Multiple Precision Arithmetic Library

Not using tabling

But we want to have a solution without relying on tabling.

Without looking at this page at all...

We have two ways of doing it:

  • Top down: Starting computation of fib(N) at N, and recursively decomposing until we reach fib(0) and fib(1). Here we have to delay computation of higher values until the lower values are known. This sounds like "Dynamic Programming" and what we want to do.
  • Bottom up: Starting computation of fib(N) at 0 and 1 (for which the results are known), and iteratively moving upwards to N. Here we know all the lower values of fib(m), 0 <= m < n, at any n, while inching towards N. Efficient but not what we want to do. (code for that is also at the bottom of this page).

So ... "top down" it is.

Top-down approaches

We need a cache in which already-computed results are stored.

Approach 1

Prolog has no "mutable data structures" (in principle), so a first approach would be: given an existing cache, we will have to construct a new cache from it, inserting a recently-computed value, and continue work with this new cache. The cache becomes a "hot potatoe" that is handed from one call to the next. Or it "threads through" the computation. It appears in two places in each argument list: Once going in, once coming out.

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

Note that the cache is actually something that is "outside the problem": it should be up-to-date and accessible by all fib calls at any time in it freshest version. In an imperative programming language, it would be a mutable structure, to which fib calls are given a reference. The Prolog way actually also makes sure that every fib call has access to the latest cache. (prove this!).

(But what if we are really going to deal with ... parallelism (as opposed to just concurrency)? Then the problems becomes vaster, and synchronization on cache access is needed. We don't go there. Much effort at fine-grained parallelism in Prolog has been abandoned)

So, the cache can be a list of length N+1 where we store the value of fib(n) at index n (list indexes go from 0) once it is know. If it is not know, we can for example store 0 there.

An alternative approach might be based on a cache built on a map. These are available in SWI-Prolog directly as "dicts" and certainly in other Prologs via libraries, but with different syntax (Dict's ain't standardized yet.).

Approach 2

But then there is a much better approach. Instead of inserting 0 for unknown-as-yet into the cache, we can insert fresh, unconstrained variables. A variable is global data that is versioned (upversioned on choicepoints, downversioned on backtracking), and it can be made "more precise" or "refined" as time progresses, for example, set to be an integer (and that's it, you cannot "unprecise" it).

After some trial-and-error we find a solution that just takes the Cache (not a CacheIn and CacheOut) and refines the fresh variable in the cache at position x to fib(x). This is very near the imperative approach of having a global cache whose values are set as computation progresses. The key is the predicate var(X) which actually checks whether X is an unrefined variable or something else (in this case, an actual integer value).

Approach 3

(This brings up another idea based not on a visible cache, but on an invisible one managed by Prolog based on coroutining and the freeze predicate. More later maybe.)

At rosettacode.org, under Fibonacci/Prolog, the following quite interesting solution is given. It uses lazy lists:

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

That's for the afternoon course of Intro to Prolog. Later.

Approach 2: Cache as a list of fresh variables, globally updated

Code

Ok, let's get this party started.

Don't think I found this on first try either. If you understand this, you understand Prolog.


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

But is it fast?

Is it time-efficient? Not as much as the tabled predicate:

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

Tests

Time for test using SWI-Prolog's test facilities

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

Running this:

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

Approach 1: Cache as a list of values, locally updated, "threaded"

Code

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

But is it fast?

Is it time-efficient? Not as much as the tabled predicate:

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

Tests

As is the custom:

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

Running this:

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


来源:https://stackoverflow.com/questions/61020478/prolog-dynamic-programming-fibonacci-series

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