为什么这个动态版本的Fibonacci程序比另一个快得多? Prolog解决方案

时间:2013-05-03 12:17:13

标签: prolog fibonacci

我正在使用SWI Prolog学习Prolog,我对Fibonacci数字计算程序的两个解决方案有疑问:

第一个就是:

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的Fibonacci数,将它保存在Prolog数据库中 asserta / 2 谓词后重用它。

因此,例如,如果我计算10的斐波纳契数和11时我计算12的斐波那契数,我将使用前2次计算的结果。

所以我的代码是:

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

在我看来,逻辑与前一个逻辑相同:

如果N> 2,则F是N的斐波那契数,并使用递归计算N的斐波那契数(如前例所示)

如果我要求计算一个已经计算过的数字的Fibonacci数并将其放入其前辈(或其中一些)的数据库Fibonacci数中,我希望程序更快但在我看来它以一种奇怪的方式工作:它太快了,能够直接计算非常大整数的Fibonacci数(另一个版本出现这么大的数字)

另一个奇怪的事情是,如果我做了一个查询的痕迹,我得到这样的东西:

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

正如您所看到的那样,似乎不执行Fibonacci谓词的代码,而是直接获取结果(从哪里?!?!)

Instad如果我执行此查询(使用第一个版本),我获得该程序将计算它:

[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 谓词的版本)将计算两个数字的斐波纳契数,并使用这些值生成下一个的解。

例如,我可能会遇到以下情况:我还没有计算任何Fibonacci数,我要求计算N = 4的Fibonacci数,所以计算它(如第二个发布的堆栈跟踪)。

所以我要求计算N = 5的Fibonacci数,并且他使用N = 4的Fibonacci它保存了。那么我要求它计算N = 6的Fibonacci数,最后它可以使用保存的斐波纳契数4和5

我错过了什么?你能帮我理解吗?

3 个答案:

答案 0 :(得分: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.

这就是它跑得这么快的原因。你看到的速度差异是the difference between an exponential and linear time complexity algorithm

下次调用它时,它可以访问以前计算的所有结果:

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

答案 1 :(得分:3)

您的第一个解决方案

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

需要注意的重要一点是Fibonacci序列只需跟踪系列中的前两个元素。为什么要在每次迭代时重新计算它们?只需保持上面的滑动窗口。

Fibonacci序列的一个更有趣的特性是当你进一步移动到序列中时,任何两个相邻值的比率越来越接近 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 )
  .

答案 2 :(得分:1)

这不是关于Prolog的,而是关于算法的。天真的递归解决方案需要O(2 ** n)个步骤来计算,而第二个版本使用memoization将其减少到O(n)。

要了解这意味着什么,请尝试在纸上计算fib(4),而不要查找先前计算过的内容。然后,再做一次但保留笔记并尽可能地查找。

之后,如果你尝试以第一种方式计算fib(5),首先必须计算fib(4)和fib(3)。这就是你的第一个算法。请注意,为了计算fib(4),您需要再次计算fib(3)。所以你最终一遍又一遍地做同样的计算。

另一方面,您可以查看这两个值并立即获得结果。这就是你的第二个算法所做的。

对于O(2 ** n),每个后续值需要两倍的工作量,而对于O(n),您只需要完成与前一个值相同的工作量。