【问题标题】:Prolog - Finding the Nth Fibonacci number using accumulatorsProlog - 使用累加器查找第 N 个斐波那契数
【发布时间】:2021-06-14 15:10:35
【问题描述】:

我有这段代码可以逆序生成斐波那契数列列表。

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 fibonacci


【解决方案1】:

我得到了这个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}|

【讨论】:

    【解决方案2】:

    给出一个“假”,因为 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;自述文件here

    【讨论】:

    • 请注意,fib2(N, Xs) 会产生一个 incomplete 答案,而原来的答案会产生两个答案,随后会产生一个实例化错误。
    • 我总是发现计数 down 非常令人困惑,当计算是自下而上进行时 - up。从0到N,不妨将计数器与N进行比较并完成它:)(只是个人的烦恼)。它在概念上也更好:目标值将不变地传递,模拟(在 Prolog 中不存在)嵌套范围;一个聪明的编译器可能甚至能以某种方式识别并使用它来优化代码。好吧,理论上。
    • @WillNess 同意,这样会更好。
    • @WillNess 更改代码以执行比较而不是倒计时。
    【解决方案3】:

    此解决方案使用的行李更少,可以随身携带。
    公式在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>

    【讨论】:

    【解决方案4】:

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

    只需将领先目标X=[H|_] 插入查询。当然,如果你真的对列表不感兴趣,你可以将两个目标合二为一

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

    您的代码执行 ~ 2N 步,这仍然是 O(N) ,就像累加器版本一样,所以,没什么大不了的,就这样就可以了。真正的区别是您的版本占用的 O(N) 空间,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-long 未实例化变量列表,直至最深的递归级别,然后在使用计算值填充列表的同时计算它们 - 但只有永远指最后 两个 斐波那契数,即该列表中的 前两个 值。因此,您不妨将其明确化,并最终得到....基于累加器的版本,您自己!

    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.
    

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

    【讨论】:

      【解决方案5】:

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

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

      【讨论】:

      • 非常好!我猜速度不是来自 if then else (->)/2,而是来自专门化 mulvec(A,A,B)。 SWI-Prolog 使用的最有可能的 GMP 可以比 B=A 的 A*B 更快地计算 A^2。不是 100% 确定。
      • 谢谢!实际上,将 ^ 替换为 * 并不会显着改变速度。因此,当您说速度的提高可能是由于矩阵乘法的专业化时,我认为您是对的。
      • Slagos 的版本称为 "fast doubling",但 MC 的版本是矩阵求幂,使用向量进行流线型。我不认为这些是相同的。
      • @DavidTonhofer 很高兴知道! :-)
      【解决方案6】:

      这个使用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>

      【讨论】:

      • 我不能马上认出它,但它的基础应该是The Linear Algebra View of the Fibonacci Sequence
      • 还有另一种解决方案,分别是对该解决方案的改进。只提高一个黄金比例,然后四舍五入。这可能需要 isqrt/2 在没有任何浮点数的情况下实现它,还不知道。
      • 奇怪的是没有人提出记忆化解决方案,就像在 Dogelog 中这样,现在有动态数据库:gist.github.com/jburse/…
      • 也许它与操作员的问题“累加器”不匹配。
      【解决方案7】:

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

      算法可以在 module 文件和 unit tests 文件中找到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 ]
      

      我们可以只保留每个矩阵的第二列而不会丢失 信息。这些向量之间的运算不是一些 像乘法这样的标准运算,让我们用⨝标记它:

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

      【讨论】:

        猜你喜欢
        • 2022-12-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-09-03
        • 2012-10-12
        • 2014-02-22
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多