Prevent backtracking after first solution to Fibonacci pair

北战南征 提交于 2019-11-30 10:08:19
false

There are two parts to get what you want.

The first is to use call_semidet/1 which ensures that there is exactly one answer. See links for an implementation for SICStus, too. In the unlikely event of having more than one answer, call_semidet/1 produces a safe error. Note that once/1 and !/0 alone simply cut away whatever there has been.

However, you will not be very happy with call_semidet/1 alone. It essentially executes a goal twice. Once to see if there is no more than one answer, and only then again to obtain the first answer. So you will get your answer much later.

The other part is to speed up your definition such that above will not be too disturbing to you. The solution suggested by CapelliC changes the algorithm altogether which is specific to your concrete function but does not extend to any other function. But it also describes a different relation.

Essentially, you found the quintessential parts already, you only need to assemble them a bit differently to make them work. But, let's start with the basics.

CLPFD as CLP(Z)

What you are doing here is still not that common to many Prolog programmers. You use finite domain constraints for general integer arithmetics. That is, you are using CLPFD as a pure substitute to the moded expression evaluation found in (is)/2, (>)/2 and the like. So you want to extend the finite domain paradigm which assumes that we express everything within finite given intervals. In fact, it would be more appropriate to call this extension CLP(Z).

This extension does not work in every Prolog offering finite domains. In fact, there is only SICStus, SWI and YAP that correctly handle the case of infinite intervals. Other systems might fail or succeed when they rather should succeed or fail - mostly when integers are getting too large.

Understanding non-termination

The first issue is to understand the actual reason why your original program did not terminate. To this end, I will use a failure slice. That is, I add false goals into your program. The point being: if the resulting program does not terminate then also the original program does not terminate. So the minimal failure slice of your (presumed) original program is:

fiborig(0,0) :- false.
fiborig(1,1) :- false.
fiborig(N,F) :-
   N #> 1,
   N1 #= N-1,
   N2 #= N-2,
   F #= F1+F2,
   fiborig(N1,F1), false,
   fiborig(N2,F2).

There are two sources for non-termination here: One is that for a given F there are infinitely many values for F1 and F2. That can be easily handled by observing that F1 #> 0, F2 #>= 0.

The other is more related to Prolog's execution mechanism. To illustrate it, I will add F2 #= 0. Again, because the resulting program does not terminate, also the original program will loop.

fiborig(0,0) :- false.
fiborig(1,1) :- false.
fiborig(N,F) :-
   N #> 1,
   N1 #= N-1,
   N2 #= N-2,
   F #= F1+F2,
   F1 #> 0,
   F2 #>= 0,
   F2 #= 0,
   fiborig(N1,F1), false,
   fiborig(N2,F2).

So the actual problem is that the goal that might have 0 as result is executed too late. Simply exchange the two recursive goals. And add the redundant constraint F2 #=< F1 for efficiency.

fibmin(0,0).
fibmin(1,1).
fibmin(N,F) :-
   N #> 1,
   N1 #= N-1,
   N2 #= N-2,
   F1 #> 0,
   F2 #>= 0,
   F1 #>= F2,
   F #= F1+F2,
   fibmin(N2,F2),
   fibmin(N1,F1).

On my lame laptop I got the following runtimes for fib(N, 377):

               SICStus                  SWI
             answer/total
fiborig:     0.090s/27.220s           1.332s/1071.380s
fibmin:      0.080s/ 0.110s           1.201s/   1.506s

Take the sum of both to get the runtime for call_semidet/1.

Note that SWI's implementation is written in Prolog only, whereas SICStus is partly in C, partly in Prolog. So when porting SWI's (actually @mat's) clpfd to SICStus, it might be comparable in speed.

There are still many things to optimize. Think of indexing, and the handling of the "counters", N, N1, N2.


Also your original program can be improved quite a bit. For example, you are unnecessarily posting the constraint F #>= N-1 three times!

If you are only interested in the first solution or know that there is at most one solution, you can use once/1 to commit to that solution:

?- once(fib(X, 377)).

+1 for using CLP(FD) as a declarative alternative to lower-level arithmetic. Your version can be used in all directions, whereas a version based on primitive integer arithmetic cannot.

I played a bit with another definition, I wrote in standard arithmetic and translated to CLP(FD) on purpose for this question.

My plain Prolog definition was

fibo(1, 1,0).
fibo(2, 2,1).
fibo(N, F,A) :- N > 2, M is N -1, fibo(M, A,B), F is A+B.

Once translated, since it take too long in reverse mode (or doesn't terminate, don't know), I tried to add more constraints (and moving them around) to see where a 'backward' computation terminates:

fibo(1, 1,0).
fibo(2, 2,1).
fibo(N, F,A) :-
  N #> 2,
  M #= N -1,
  M #>= 0,  % added
  A #>= 0,  % added
  B #< A,   % added - this is key
  F #= A+B,
  fibo(M, A,B). % moved - this is key

After adding B #< A and moving the recursion at last call, now it works.

?- time(fibo(U,377,Y)).
% 77,005 inferences, 0.032 CPU in 0.033 seconds (99% CPU, 2371149 Lips)
U = 13,
Y = 233 ;
% 37,389 inferences, 0.023 CPU in 0.023 seconds (100% CPU, 1651757 Lips)
false.

edit To account for 0 based sequences, add a fact

fibo(0,0,_).

Maybe this explain the role of the last argument: it's an accumulator.

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