Prolog - 使用累加器查找第 N 个斐波那契数

Prolog - Finding the Nth Fibonacci number using accumulators

我有这段代码可以以相反的顺序生成斐波那契数列列表。

fib2(0, [0]).
fib2(1, [1,0]).
fib2(N, [R,X,Y|Zs]) :-
    N > 1,
    N1 is N - 1,
    fib2(N1, [X,Y|Zs]),
    R is X + Y.

虽然我只需要第一个元素。问题是这段代码还在列表后给出了一个 false. ,所以我获取第一个元素的所有尝试都失败了。有没有什么办法可以得到列表中的第一个元素,或者有什么其他方法可以用累加器计算第 N 个斐波那契数。

提前致谢。

给出一个“假” 因为 Prolog 不确定在它提供的第一个解决方案之后是否还有更多解决方案:

?- fib2(4,L).
L = [3,2,1,1,0] ;  % maybe more solutions? 
false.             % no

这不是问题:你可以告诉Prolog在第一个之后确实没有更多的解决方案(或者你不想看到它们):

?- once(fib2(4,L)).

?- fib2(4,L),!.

或者你可以切入每个第一个子句,告诉 Prolog 如果头部匹配,就没有必要尝试另一个子句。这摆脱了流浪的“可能的解决方案”:

fib2(0, [0])   :- !.
fib2(1, [1,0]) :- !.
fib2(N, [R,X,Y|Zs]) :-
    N > 1,
    N1 is N - 1,
    fib2(N1, [X,Y|Zs]),
    R is X + Y.

可能有问题的是给定的算法存储了所有的fib(i)并在递归调用后进行加法,这意味着Prolog无法优化递归调用进入循环。

对于“基于累加器”(自下而上)的计算方式 fib(N):

% -------------------------------------------------------------
% Proceed bottom-up, without using any cache, or rather a cache
% consisting of two additional arguments.
%
% ?- fib_bottomup_direct(10,F).
% F = 55.
% ------------------------------------------------------------

fib_bottomup_direct(N,F) :-
   N>0,
   !,
   const(fib0,FA),
   const(fib1,FB),
   up(1,N,FA,FB,F).
fib_bottomup_direct(0,F0) :-
   const(fib0,F0).

% Carve the constants fib(0) and fib(1) out of the code.

const(fib0,0).
const(fib1,1).

% Tail recursive call moving "bottom up" towards N.
%
% X:  the "current point of progress"
% N:  the N we want to reach
% FA: the value of fib(X-1)
% FB: the value of fib(X)
% F:  The variable that will receive the final result, fib(N)

up(X,N,FA,FB,F) :-
   X<N, % not there yet, compute fib(X+1)
   !,
   FC is FA + FB,
   Xn is X  + 1,
   up(Xn,N,FB,FC,F).
up(N,N,_,F,F).

然后:

?- fib_bottomup_direct(11,X).
X = 89.

另外几个算法here; a README here

我得到了这个 logarithmic steps O(log n) 解决方案,甚至尾递归。
只是为了好玩,它还可以计算第 n 个卢卡斯数:

<pre id="in">
fib(N, X) :-
   powmat(N, [[0,1],[1,1]], [[1,0],[0,1]], 
             [[_,X],[_,_]]).
luc(N, Z) :-
   powmat(N, [[0,1],[1,1]], [[1,0],[0,1]], 
             [[X,Y],[_,_]]), Z is 2*X+Y.

powmat(0, _, R, R) :- !.
powmat(N, A, R, S) :- N rem 2 =\= 0, !,
   mulmat(A, R, H), M is N//2, mulmat(A, A, B), powmat(M, B, H, S).
powmat(N, A, R, S) :- 
   M is N//2, mulmat(A, A, B), powmat(M, B, R, S).

mulmat([[A11,A12],[A21,A22]], 
       [[B11,B12],[B21,B22]],
       [[C11,C12],[C21,C22]]) :-
   C11 is A11*B11+A12*B21,
   C12 is A11*B12+A12*B22,
   C21 is A21*B11+A22*B21,
   C22 is A21*B12+A22*B22.

?- fib(100,X).
?- luc(100,X).
</pre>
<script src="http://www.dogelog.ch/lib/exchange.js"></script>

您可以比较:

https://www.wolframalpha.com/input/?i=Fibonacci[100]
https://www.wolframalpha.com/input/?i=LucasN[100]

编辑 28.06.2021:
这是一个非常快速的解释为什么矩阵算法有效。 我们只需要证明斐波那契的一步是线性的。即 这种递归关系导致线性矩阵:

     F_{n+2} = F_{n}+F_{n+1}

要查看矩阵,我们必须假设矩阵 M 将向量 b=[Fn,Fn+1] 转换为向量 b'=[F_{n+1}, F_{n+2}]:

      b' = M*b

这个矩阵可能是什么?解决一下:

    |F_{n+1}|   |0*F_{n}+1*F_{n+1}|    |0  1|   |F_{n}  |
    |       | = |                 | =  |    | * |       |
    |F_{n+2}|   |1*F_{n}+1*F_{n+1}|    |1  1|   |F_{n+1}|

此解决方案使用了一种无行李箱,可以随身携带。
公式位于 wiki fibmat section:

的末尾

<pre id="in">
fib(N, X) :-
   powvec(N, (1,0), (0,1), (X,_)).
luc(N, Z) :-
   powvec(N, (1,0), (0,1), (X,Y)), Z is X+2*Y.

powvec(0, _, R, R) :- !.
powvec(N, A, R, S) :- N rem 2 =\= 0, !,
   mulvec(A, R, H), M is N//2, mulvec(A, A, B), powvec(M, B, H, S).
powvec(N, A, R, S) :- 
   M is N//2, mulvec(A, A, B), powvec(M, B, R, S).

mulvec((A1,A2), (B1,B2), (C1,C2)) :-
   C1 is A1*(B1+B2)+A2*B1,
   C2 is A1*B1+A2*B2.

?- fib(100,X).
?- luc(100,X).
</pre>
<script src="http://www.dogelog.ch/lib/exchange.js"></script>

fib2(120,X), X=[H|_], !. 回答了您的问题,将 H 绑定到该反向列表的头部,因此,第 120 个斐波那契数。

只需将抢眼目标 X=[H|_] 插入 查询。当然如果你真的对榜单不感兴趣,也可以把两个目标合二为一

fib2(120,[H|_]), !.

你的代码执行了 ~ 2N 步,这仍然是 O(N) 就像累加器版本一样,所以,没什么大不了的,它很好。真正的区别是 O(N) space 你的版本,v. 累加器的 O(1)。

但是如果你仔细查看你的代码,

fib2(0, [0]).
fib2(1, [1,0]).
fib2(N, [R,X,Y|Zs]) :-
    N > 1,
    N1 is N - 1,
    fib2(N1, [X,Y|Zs]),
    R is X + Y.

你意识到它在向下递归的最深层次的过程中创建了 N 长的未实例化变量列表,然后计算它们,同时在返回的过程中用计算值填充列表 - - 但只引用最后的 两个 斐波那契数,即该列表中的 前两个 值。所以你最好把它说清楚,最后得到......一个基于累加器的版本,你自己!

fib3(0, 0, 0).
fib3(1, 1, 0).
fib3(N, R, X) :-
    N > 1,
    N1 is N - 1,
    fib3(N1, X, Y),
    R is X + Y.

除了它仍然不是尾递归的。实现这一点的方法通常是使用附加参数,您可以在另一个 here, by David Tonhofer 中看到这样的代码。但希望你现在能看到它和最后一个之间的清晰路径。

只是为了好玩,下面展示了一个甚至更快的斐波那契版本(即使不使用尾递归):

% -----------------------------------------------------------------------
% FAST FIBONACCI
% -----------------------------------------------------------------------

ffib(N, F) :-
    ff(N, [_, F]).

ff(1, [0, 1]) :- !.
ff(N, R) :-
    M is N // 2,
    ff(M, [A, B]),
    F1 is A^2   + B^2,
    F2 is 2*A*B + B^2,
    (   N mod 2 =:= 0
    ->  R = [F1, F2]
    ;   F3 is F1 + F2,
        R = [F2, F3]   ).

% -----------------------------------------------------------------------
% MOSTOWSKI COLLAPSE VERSION
% -----------------------------------------------------------------------

fib(N, X) :-
   powvec(N, (1,0), (0,1), (X,_)).

powvec(0, _, R, R) :- !.

powvec(N, A, R, S) :-
   N rem 2 =\= 0, !,
   mulvec(A, R, H),
   M is N // 2,
   mulvec(A, A, B),
   powvec(M, B, H, S).

powvec(N, A, R, S) :-
   M is N // 2,
   mulvec(A, A, B),
   powvec(M, B, R, S).

mulvec((A1,A2), (B1,B2), (C1,C2)) :-
   C1 is A1*(B1 + B2) + A2*B1,
   C2 is A1*B1 + A2*B2.

% -----------------------------------------------------------------------
% COMPARISON
% -----------------------------------------------------------------------

comparison :-
   format('n     fib   ffib  speed~n'),
   forall( between(21, 29, E),
      (  N is 2^E,
         cputime(fib( N, F1), T1),
         cputime(ffib(N, F2), T2),
         F1 = F2,        % confirm that both versions compute same answer!
         catch(R is T1/T2, _, R = 1),
         format('2^~w~|~t~2f~6+~|~t~2f~6+~|~t~2f~6+~n', [E, T1, T2, R]))).

cputime(Goal, Time) :-
   T0 is cputime,
   call(Goal),
   Time is cputime - T0.

两个版本(我的和@MostowskiCollapse 的)的时间复杂度O(lg n), 忽略乘法成本。

使用 SWI-Prolog 版本 8.2.4 获得的一些简单经验结果(以秒为单位的时间):

?- comparison.
n     fib   ffib  speed
2^21  0.05  0.02  3.00
2^22  0.09  0.05  2.00
2^23  0.22  0.09  2.33
2^24  0.47  0.20  2.31
2^25  1.14  0.45  2.52
2^26  2.63  1.02  2.58
2^27  5.89  2.34  2.51
2^28 12.78  5.28  2.42
2^29 28.97 12.25  2.36
true.

基于 Donald Knuth 的 Fibonacci-by-matrix 乘法方法,由 Mostowski Collapse,但更明确。

算法可以在模块文件加上单元测试文件on github:

中找到

该原理基于 Donald Knuth 提供的矩阵恒等式(在 Donald E. Knuth. The Art of Computer Programming. Volume 1. Fundamental 算法,第二版第 80 页)

对于 n >= 1 我们有(对于 n=0,单位矩阵出现在右侧,但不清楚 fib(-1) 是什么):

                                  n
  [ fib(n+1) fib(n)   ]   [ 1  1 ]
  [                   ] = [      ]
  [ fib(n)   fib(n-1) ]   [ 1  0 ]

但是如果我们使用常量 fib(0)fib(1) 而不假设它们的值分别为 0 和 1 (我们可能正在处理一个特殊的斐波那契数列),那么我们必须规定对于 n >= 1:

                                                       n-1
  [ fib(n+1) fib(n)   ]   [ fib(2) fib(1) ]   [ 1  1 ]
  [                   ] = [               ] * [      ]
  [ fib(n)   fib(n-1) ]   [ fib(1) fib(0) ]   [ 1  0 ]

我们将单独计算右侧的“幂矩阵”并显式地与“斐波那契起始矩阵”相乘,因此:

const(fib0,0).
const(fib1,1).

fib_matrixmult(N,F) :-
   N>=1,
   !,
   Pow is N-1,
   const(fib0,Fib0),
   const(fib1,Fib1),
   Fib2 is Fib0+Fib1,
   matrixpow(
      Pow,
      [[1,1],[1,0]],
      PowMx),
   matrixmult(
      [[Fib2,Fib1],[Fib1,Fib0]],
      PowMx,
      [[_,F],[F,_]]).
fib_matrixmult(0,Fib0) :-
   const(fib0,Fib0).

matrixpow(Pow, Mx, Result) :-
   matrixpow_2(Pow, Mx, [[1,0],[0,1]], Result).

matrixpow_2(Pow, Mx, Accum, Result) :-
   Pow > 0,
   Pow mod 2 =:= 1,
   !,
   matrixmult(Mx, Accum, NewAccum),
   Powm is Pow-1,
   matrixpow_2(Powm, Mx, NewAccum, Result).
matrixpow_2(Pow, Mx, Accum, Result) :-
   Pow > 0,
   Pow mod 2 =:= 0,
   !,
   HalfPow is Pow div 2,
   matrixmult(Mx, Mx, MxSq),
   matrixpow_2(HalfPow, MxSq, Accum, Result).
matrixpow_2(0, _, Accum, Accum).

matrixmult([[A11,A12],[A21,A22]],
           [[B11,B12],[B21,B22]],
           [[C11,C12],[C21,C22]]) :-
   C11 is A11*B11+A12*B21,
   C12 is A11*B12+A12*B22,
   C21 is A21*B11+A22*B21,
   C22 is A21*B12+A22*B22.

如果您的起始矩阵肯定是 [[1,1],[1,0]],您可以将主谓词中的两个操作 matrixpow/3 和后面的 matrixmult/3 合并为对 matrixpow/3 的单个调用.

上述算法计算“太多”,因为斐波那契数列矩阵中的两个值可以从另外两个推导出来。我们可以摆脱这种冗余。 Mostowski Collapse 提出了一个紧凑的算法来做到这一点。为了便于理解,下文进行了扩展:

我们的想法是摆脱 matrixmult/3 中的冗余操作,方法是利用我们所有的矩阵都是对称的并且实际上持有 斐波那契数

[ fib(n+1)  fib(n)   ]
[                    ]
[ fib(n)    fib(n-1) ]

所以,如果我们将矩阵 A 和 B 相乘得到 C,我们总是有这种形式的东西(即使在初始情况下 B 是 单位矩阵):

[ A1+A2  A1 ]   [ B1+B2  B1 ]   [ C1+C2  C1 ]
[           ] * [           ] = [           ]
[ A1     A2 ]   [ B1     B2 ]   [ C1     C2 ]

我们可以只保留每个矩阵的第二列 w/o 损失 信息。这些向量之间的操作不是一些 像乘法这样的标准运算,我们用⨝来标记:

[ A1 ]    [ B1 ]   [ C1 ]
[    ] ⨝ [    ] = [    ]
[ A2 ]    [ B2 ]   [ C2 ]

其中:

C1 = B1*(A1+A2) + B2*A1 or A1*(B1+B2) + A2*B1
C2 = A1*B1 + A2*B2
fib_matrixmult_streamlined(N,F) :-
   N>=1,
   !,
   Pow is N-1,
   const(fib0,Fib0),
   const(fib1,Fib1),
   matrixpow_streamlined(
      Pow,
      v(1,0),
      PowVec),
   matrixmult_streamlined(
      v(Fib1,Fib0),
      PowVec,
      v(F,_)).
fib_matrixmult_streamlined(0,Fib0) :-
   const(fib0,Fib0).

matrixpow_streamlined(Pow, Vec, Result) :-
   matrixpow_streamlined_2(Pow, Vec, v(0,1), Result).

matrixpow_streamlined_2(Pow, Vec, Accum, Result) :-
   Pow > 0,
   Pow mod 2 =:= 1,
   !,
   matrixmult_streamlined(Vec, Accum, NewAccum),
   Powm is Pow-1,
   matrixpow_streamlined_2(Powm, Vec, NewAccum, Result).
matrixpow_streamlined_2(Pow, Vec, Accum, Result) :-
   Pow > 0,
   Pow mod 2 =:= 0,
   !,
   HalfPow is Pow div 2,
   matrixmult_streamlined(Vec, Vec, VecVec),
   matrixpow_streamlined_2(HalfPow, VecVec, Accum, Result).
matrixpow_streamlined_2(0, _, Accum, Accum).

matrixmult_streamlined(v(A1,A2),v(B1,B2),v(C1,C2)) :-
   C1 is A1*(B1+B2) + A2*B1,
   C2 is A1*B1 + A2*B2.

这个使用Golden Ratio公式:

<pre id="in">
fib(N, S) :-
   powrad(N,(1,1),(1,0),(_,X)),
   powrad(N,(1,-1),(1,0),(_,Y)),
   S is (X-Y)//2^N.
luc(N, S) :-
   powrad(N,(1,1),(1,0),(X,_)),
   powrad(N,(1,-1),(1,0),(Y,_)),
   S is (X+Y)//2^N.

powrad(0, _, R, R) :- !.
powrad(N, A, R, S) :- N rem 2 =\= 0, !,
   mulrad(A, R, H), M is N//2, mulrad(A, A, B), powrad(M, B, H, S).
powrad(N, A, R, S) :-
   M is N//2, mulrad(A, A, B), powrad(M, B, R, S).

mulrad((A,B),(C,D),(E,F)) :-
   E is A*C+B*D*5,
   F is A*D+B*C.

?- fib(100,X).
?- luc(100,X).
</pre>
<script src="http://www.dogelog.ch/lib/exchange.js"></script>