我正在使用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
我错过了什么?你能帮我理解吗?
答案 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),您只需要完成与前一个值相同的工作量。