快速计算斐波那契数列

7
我几周前在Google+上看到一条评论,其中有人展示了一种简单的斐波那契数列计算方法,它不基于递归,也没有使用记忆化。他只需记住最后两个数字并将它们相加。这是一个O(n)算法,但他实现得非常干净。所以我很快指出更快的方法是利用它们可以作为[[0,1],[1,1]]矩阵的幂次来计算,这只需要进行O(log(N))计算。
当然,问题在于这远非在某一点过后的最优解。只要数字不太大,它就是有效的,但它们的长度以N*log(phi)/log(10)的速率增长,其中N是第N个斐波那契数,phi是黄金比例((1+sqrt(5))/2约为1.6)。事实证明,log(phi)/log(10)非常接近1/5。因此,第N个斐波那契数可以预计具有大约N/5位数。
矩阵乘法,甚至数字相乘,当数字开始变成数百万或数十亿位时,速度会变得非常慢。因此,在Python中计算F(100,000)大约需要0.03秒,而计算F(1000,000)则需要大约5秒。这几乎不是O(log(N))增长。我的估计是,即使没有改进,该方法也仅将计算优化为O((log(N))^(2.5))左右。
以这种速度计算十亿级的斐波那契数列,将会非常缓慢(即使它只有约1,000,000,000/5位数字,因此轻松适合32位内存)。
是否有人知道一种实现或算法,可以允许更快的计算?也许有些东西可以计算万亿级的斐波那契数。
只是为了明确,我不是在寻找近似值。我正在寻找精确计算(到最后一位)。
编辑1:我正在添加Python代码,以显示我认为是O((log N)^ 2.5)算法。
from operator import mul as mul
from time import clock

class TwoByTwoMatrix:
    __slots__ = "rows"

    def __init__(self, m):
        self.rows = m

    def __imul__(self, other):
        self.rows = [[sum(map(mul, my_row, oth_col)) for oth_col in zip(*other.rows)] for my_row in self.rows]
        return self

    def intpow(self, i):
        i = int(i)
        result = TwoByTwoMatrix([[long(1),long(0)],[long(0),long(1)]])
        if i <= 0:
            return result
        k = 0
        while i % 2 == 0:
            k +=1
            i >>= 1
        multiplier = TwoByTwoMatrix(self.rows)
        while i > 0:
            if i & 1:
                result *= multiplier
            multiplier *= multiplier # square it
            i >>= 1
        for j in xrange(k):
            result *= result
        return result


m = TwoByTwoMatrix([[0,1],[1,1]])

t1 = clock()
print len(str(m.intpow(100000).rows[1][1]))
t2 = clock()
print t2 - t1

t1 = clock()
print len(str(m.intpow(1000000).rows[1][1]))
t2 = clock()
print t2 - t1

编辑2: 看来我没有考虑到len(str(...))会对测试的总运行时间产生显著的贡献。将测试更改为

from math import log as log

t1 = clock()
print log(m.intpow(100000).rows[1][1])/log(10)
t2 = clock()
print t2 - t1

t1 = clock()
print log(m.intpow(1000000).rows[1][1])/log(10)
t2 = clock()
print t2 - t1

缩短了运行时间为0.008秒和0.31秒(从使用len(str(...))时的0.03秒和5秒)。

因为M = [[0,1],[1,1]]的N次方等于[[F(N-2), F(N-1)],[F(N-1), F(N)]], 另一个明显的低效率来源是将矩阵的(0,1)和(1,0)元素计算为不同的元素。这样做的效率较低(我已经切换到Python3,但Python2.7的时间类似):

class SymTwoByTwoMatrix():
    # elments (0,0), (0,1), (1,1) of a symmetric 2x2 matrix are a, b, c.
    # b is also the (1,0) element because the matrix is symmetric

    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c

    def __imul__(self, other):
        # this multiplication does work correctly because we 
        # are multiplying powers of the same symmetric matrix
        self.a, self.b, self.c = \
            self.a * other.a + self.b * other.b, \
            self.a * other.b + self.b * other.c, \
            self.b * other.b + self.c * other.c
        return self

    def intpow(self, i):
        i = int(i)
        result = SymTwoByTwoMatrix(1, 0, 1)
        if i <= 0:
            return result
        k = 0
        while i % 2 == 0:
            k +=1
            i >>= 1
        multiplier = SymTwoByTwoMatrix(self.a, self.b, self.c)
        while i > 0:
            if i & 1:
                result *= multiplier
            multiplier *= multiplier # square it
            i >>= 1
        for j in range(k):
            result *= result
        return result

在0.006秒内计算出F(100,000),在0.235秒内计算出F(1,000,000),在9.51秒内计算出F(10,000,000),这是可以预期的。最快测试结果比预期快45%,预计增益应该渐近地接近phi /(1 + 2 * phi + phi * phi)〜23.6%。

M ^ N的(0,0)元素实际上是第N-2个斐波那契数:

for i in range(15):
    x = m.intpow(i)
    print([x.a,x.b,x.c])

提供

[1, 0, 1]
[0, 1, 1]
[1, 1, 2]
[1, 2, 3]
[2, 3, 5]
[3, 5, 8]
[5, 8, 13]
[8, 13, 21]
[13, 21, 34]
[21, 34, 55]
[34, 55, 89]
[55, 89, 144]
[89, 144, 233]
[144, 233, 377]
[233, 377, 610]

我希望不需要计算元素(0,0)会使速度提高额外的1 /(1 + phi + phi * phi)〜19%。但是由Eli Korvigo提供的F(2N)和F(2N-1)的 lru_cache solution given by Eli Korvigo below实际上加速了4倍(即75%)。因此,虽然我没有找到正式的解释,但我倾向于认为它缓存了N的二进制展开中1的跨度,并执行必要的最小乘法次数。这就避免了查找那些范围,在正确的时刻在N的展开中预先计算它们并将它们相乘。 lru_cache 允许从上到下计算本来会更复杂的自下而上计算。

每次N增长10倍,SymTwoByTwoMatrix和lru_cache-of-F(2N)-and-F(2N-1)的计算时间大致增加了40倍。我认为这可能是由于Python对长整数乘法的实现。我认为大数的乘法和加法应该是可并行化的。因此,即使F(N)的解决方案是Theta(n)(如Daniel Fisher在评论中所述),也应该可以实现一个多线程的子O(N)解决方案。


2
由于 F(n)Theta(n) 位(无论在哪个基础上的数字),因此您 无法O(n) 更快地计算它。 - Daniel Fischer
3
嘿, 把它弄错了,这仍然是一个 CS 问题,不是一个 SO 问题。它在这里是不相关的话题。 - Martijn Pieters
3
你正在寻求算法,而不是代码。 - Martijn Pieters
2
您可能也对此答案中的算法感兴趣。 - Daniel Fischer
1
我的原始 Haskell 实现在不到一分钟的时间内计算出了十亿位数(也是基于矩阵乘法),并在大约 12 分钟内计算出了第 100 亿位数。这里有一个更快的实现(链接:https://wiki.haskell.org/The_Fibonacci_sequence),可以在 3 分钟内计算出第 100 亿位数。第一万亿位数大约有 2000 亿位数字。如果你有一台拥有 200 GB RAM 的机器,我敢肯定这个算法会在不到一周的时间内计算出来(将参数增加 10 倍会使计算时间增加约 12 倍)。 - n. m.
显示剩余6条评论
4个回答

7

由于斐波那契数列是线性递归,因此可以通过闭合形式计算其成员。这涉及计算幂,类似于矩阵乘法解决方案可以在O(logn)中完成,但常数开销应该更低。这是我知道的最快的算法。

fib

编辑

抱歉,我错过了“精确”这一部分。矩阵乘法的另一个精确O(log(n))替代方法可以按以下方式计算

fib2

from functools import lru_cache

@lru_cache(None)
def fib(n):
    if n in (0, 1):
        return 1
    if n & 1:  # if n is odd, it's faster than checking with modulo
        return fib((n+1)//2 - 1) * (2*fib((n+1)//2) - fib((n+1)//2 - 1))
    a, b = fib(n//2 - 1), fib(n//2)
    return a**2 + b**2

这是基于Edsger Dijkstra教授的笔记推导而来的。该解决方案利用了计算F(2N)和F(2N-1)所需的仅为F(N)和F(N-1)的事实。尽管如此,您仍然需要处理长数字算术,但开销应该比基于矩阵的解决方案小。在Python中,由于缓存和递归速度较慢,最好以命令式风格重写此代码,尽管我之前以函数式形式进行了编写以提高清晰度。

1
也许你可以使用Sympy来处理这个式子,以获得无理数的解析答案而不会出现舍入误差。 - Aaron
1
关键在于精确计算。 - Dmitry Rubanovich
我也不理解为什么您有两个不同的实现,一个用于奇数,另一个用于偶数。 - Alexandros Spyropoulos
@AlexandrosSpyropoulos 你的意思是你不理解我提供的 Dijkstra 推导吗? - Eli Korvigo
@Eli Korvigo,我也注意到了这一点,但是这个帖子活跃后已经过了一段时间。我不确定这是否在不同版本的Python之间发生了变化。不知何故(在我脑海的某个角落),我记得当数字超出int值范围时,使用**时会得到浮点结果。而在使用普通算术运算符时,我得到的结果是任意大的整数。我不确定它是否有足够的趣味性来进行检查。 - Dmitry Rubanovich
显示剩余13条评论

2
这段话的英译是:

这篇评论太长了,所以我会留一个答案。

Aaron 的回答是正确的,而且我已经点赞了,你也应该点赞。我将提供相同的答案,并解释为什么它不仅正确,而且是目前发布的最佳答案。我们正在讨论的公式是:

formula

计算Φ的复杂度为O(M(n)),其中M(n)是乘法的复杂度(目前略高于线性对数级),n是位数。
然后有一个幂函数,可以表示为对数(O(M(n)•log(n))的复杂度、乘法(O(M(n)))的复杂度和指数(O(M(n)•log(n))的复杂度。
然后有一个平方根(O(M(n))),一个除法(O(M(n))),和一个最终轮(O(n))。
这使得这个答案在n位时大约为O(n•log^2(n)•log(log(n)))
我还没有彻底分析除法算法,但如果我理解正确的话,每个位可能需要递归(你需要将数字除以log(2^n)=n次),每个递归需要一个乘法。因此它的时间复杂度不会比O(M(n)•n)更好,而这是指数级别恶化的。

1
如果你想编辑他的答案,以便更清楚地扩展它超出双浮点精度,那么你可能可以使其更清晰。但我没有看到这一点。 - Dmitry Rubanovich

2
使用其他答案中的奇怪平方根公式closed form fibo,您可以精确计算第k个斐波那契数。这是因为最终会消除$\sqrt(5)$。您只需安排好乘法,同时跟踪它即可。
def rootiply(a1,b1,a2,b2,c):
    ''' multipy a1+b1*sqrt(c) and a2+b2*sqrt(c)... return a,b'''
    return a1*a2 + b1*b2*c, a1*b2 + a2*b1

def rootipower(a,b,c,n):
    ''' raise a + b * sqrt(c) to the nth power... returns the new a,b and c of the result in the same format'''
    ar,br = 1,0
    while n != 0:
        if n%2:
            ar,br = rootiply(ar,br,a,b,c)
        a,b = rootiply(a,b,a,b,c)
        n /= 2
    return ar,br

def fib(k):
    ''' the kth fibonacci number'''
    a1,b1 = rootipower(1,1,5,k)
    a2,b2 = rootipower(1,-1,5,k)
    a = a1-a2
    b = b1-b2
    a,b = rootiply(0,1,a,b,5)
    # b should be 0!
    assert b == 0
    return a/2**k/5

if __name__ == "__main__":
    assert rootipower(1,2,3,3) == (37,30) # 1+2sqrt(3) **3 => 13 + 4sqrt(3) => 39 + 30sqrt(3)
    assert fib(10)==55

首先,我认为这个算法的时间复杂度是O(log(n)),空间复杂度是常数级别的。最佳答案在执行结束时栈上会有log(n)的内容。此外,我更希望得到一个通用的答案……虽然这段代码对于计算斐波那契数列很方便,但是同样的技巧也可以应用于许多线性递推关系(其中少于四个项的递推关系至少应该具有根式解)。 - Him
@Scott,我还没有完全阅读它,但我认为我应该提几点。首先,对于大于5 *(计算是原子的任何数字的大小)的n,没有解决方案将是O(log(n))。这是因为Fib(n)大约有n / 5位数字(因为log(phi)/ log(10)〜.2)。因此,在计算最后一个数字时涉及的乘法将是整个计算中最长的操作。其次,Eli Korvogo的解决方案利用了线性递推矩阵得出的公式,并且他的解决方案缓存了中间结果。 - Dmitry Rubanovich
这个解决方案中没有需要缓存的中间结果。如果您仔细查看此解决方案,您会发现没有递归调用。 - Him
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Dmitry Rubanovich
1
@Scott,好的,我已经阅读了你的代码,并将操作重构为一个实际的类,该类可以执行Q[sqrt(5)]算术运算。使用这个类,我在计算第1,000,000个斐波那契数时始终保持1.68秒。使用你编写的函数,我在计算相同的斐波那契数时得到2.959秒。在同一系统上,使用我上面概述的SymMatrix方法,我在计算第1,000,000个斐波那契数时只需要0.12秒。因此,虽然你关于约分的观察是一个很好的观察,但它并不能证明在无法原子化地进行算术运算的数字上使用这种计算方法是有道理的。 - Dmitry Rubanovich
显示剩余6条评论

-1

维基百科得知,

对于所有n ≥ 0,数列Fn是最接近phi^n/sqrt(5)的整数,其中phi是黄金比例。因此,可以通过四舍五入来找到它,即使用最近整数函数。


4
但是phi和sqrt(5)都是无理数,因此它们的值在末尾会丢失一些信息。我正在寻找精确计算。 - Dmitry Rubanovich
当您所需的精度随着数字位数的增加而增加时,您可能不得不接受执行时间也会增加的事实... - Aaron
我相信在x86平台上,借助预先构建的大数代码,这应该是相对容易实现的。 - Aaron
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Dmitry Rubanovich
不,我的意思是使用x86从1、1、2、3、5开始计数...消除简单累加和存储操作的语言开销。 - Aaron
从F(1000000)到F(2000000)的直接加法需要对数量级为200,000-400,000的数字进行一百万次加法。而使用矩阵的简单乘法,只需要进行1次平方运算,这意味着对那个数量级的数字进行4次加法和8次乘法。是否可以避免这些乘法超过计算量并可能并行化是我不确定的事情。 - Dmitry Rubanovich

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接