【问题标题】:My program can't run that fast even with memoization即使有记忆,我的程序也不能运行得那么快
【发布时间】:2020-06-28 18:25:17
【问题描述】:

我在项目 euler 上尝试了一个问题,我需要找到 400 万以下的所有斐波那契项的总和。我花了很长时间,但后来我发现我可以使用 memoization 来做到这一点,但似乎还需要很长时间。经过大量研究,我发现我可以使用一个名为 lru_cache 的内置模块。我的问题是:为什么它不像记忆一样快?

这是我的代码:

from functools import lru_cache


@lru_cache(maxsize=1000000)
def fibonacci_memo(input_value):
    global value
    fibonacci_cache = {}
    if input_value in fibonacci_cache:
        return fibonacci_cache[input_value]
    if input_value == 0:
        value = 1
    elif input_value == 1:
        value = 1
    elif input_value > 1:
        value = fibonacci_memo(input_value - 1) + fibonacci_memo(input_value - 2)
        fibonacci_cache[input_value] = value
    return value


def sumOfFib():
    SUM = 0
    for n in range(500):
        if fibonacci_memo(n) < 4000000:
            if fibonacci_memo(n) % 2 == 0:
                SUM += fibonacci_memo(n)
    return SUM


print(sumOfFib())

顺便说一句,代码可以工作。当我使用 lru_cache 模块时,运行它只需不到一秒钟。

【问题讨论】:

  • 总和满足常阶线性递推关系。找到它并直接计算总和。
  • 这个问题可以在 O(n) 内有效地解决,方法是将当前总和和内存中的最后 2 个斐波那契值保持在一个循环中,直到达到高于 40 亿的数字。
  • 更进一步,如果将递归表示为矩阵乘法,则可以在 O(ln(n)) 中求解。
  • 我花了很长时间,但后来我发现我可以使用 memoization 来做到这一点,但似乎还需要很长时间。经过大量研究,我发现我可以使用一个名为 lru_cache 的内置模块。我的问题是:为什么它不如 memoization 快? 不使用 lru_cache memoization 吗?我不确定我是否了解您共享的代码中两者之间的关系。

标签: python fibonacci


【解决方案1】:

确实,另一个答案是计算斐波那契数列的正确方法,但您也应该知道为什么您的记忆不起作用。具体来说:

fibonacci_cache = {}

函数内部的这一行意味着每次调用 fibonacci_memo 时都在清空缓存。

【讨论】:

    【解决方案2】:

    您不应该计算斐波那契数列,甚至不应该通过动态编程。由于斐波那契数列满足常数系数和常数阶的线性递推关系,那么它们的和的序列也是如此。

    绝对不要缓存所有值。这会给您带来不必要的内存消耗。当循环具有恒定顺序时,您只需要记住与循环的顺序一样多的先前术语。

    此外,还有一种方法可以将恒定顺序的递归转换为一阶的系统递归。后者的解由矩阵的幂给出。对于较大的 n 值,这提供了更快的算法。不过,每一步都会更昂贵。因此,最好的方法是结合使用这两种方法,对于较小的 n 值选择第一种方法,对于较大的输入选择第二种方法。

    O(n) 使用循环求和

    表示S_n=F_0+F_1+...+F_n是第一个斐波那契数的总和F_0,F_1,...,F_n

    注意

    • S_{n+1}-S_n=F_{n+1}
    • S_{n+2}-S_{n+1}=F_{n+2}
    • S_{n+3}-S_{n+2}=F_{n+3}

    因为F_{n+3}=F_{n+2}+F_{n+1} 我们得到了S_{n+3}-S_{n+2}=S_{n+2}-S_n。所以

    S_{n+3}=2S_{n+2}-S_n

    初始条件为S_0=F_0=1S_1=F_0+F_1=1+1=2S_2=S_1+F_2=2+2=4

    您可以做的一件事是自下而上计算S_n,在每一步仅记住前三个项的值。您不需要记住S_k 的所有值,从k=0k=n。这为您提供了具有O(1) 内存量的O(n) 算法。


    O(ln(n)) 通过矩阵求幂

    您也可以通过以下方式获得O(ln(n))算法:

    调用X_n作为具有S_{n+2},S_{n+1},S_{n}分量的列向量

    所以,上面的递归给出了递归

    X_{n+1}=AX_n

    A 是矩阵

    [
     [2,0,-1],
     [1,0,0],
     [0,1,0],
    ]
    

    因此,X_n=A^nX_0。我们有X_0。要乘以A^n,我们可以做exponentiation by squaring

    【讨论】:

    • 全部正确,但与 OP 的编程问题并不真正相关。这件事不可能花费近一秒钟的时间。
    • @IgorRivin 它没有指出 OP 代码中的实际错误,是的。尽管如此,我认为展示这样做的“正确方式”并没有什么害处。
    • 顺便说一句,矩阵的做法虽然可爱又聪明,但实际上并没有你想象的那么好。如果您使用朴素乘法(vs FFT),它会为您购买一个比愚蠢方法更稳定的因子。
    • @IgorRivin 抱歉,我不太明白。您能解释一下 FFT 是如何出现的吗?
    • @PaulPanzer 当数字变大时,有效地执行乘法变得很重要。例如,您可以将数字相乘视为在基数 102 上计算的多项式相乘。然后你可以使用 FFT。理论上,现在有asymptotically better methods
    【解决方案3】:

    为了完整起见,这里是@NotDijkstra 的回答中描述的一般思想的实现以及我的谦虚优化,包括以整数算术实现的“封闭形式”解决方案。

    我们可以看到,“智能”方法不仅速度快了一个数量级,而且似乎可以更好地扩展与 Python 大整数使用比简单乘法更好的事实(感谢 @NotDijkstra)兼容。

    import numpy as np
    import operator as op
    from simple_benchmark import BenchmarkBuilder, MultiArgument
    B = BenchmarkBuilder()
    
    def pow(b,e,mul=op.mul,unit=1):
        if e == 0:
            return unit
        res = b
        for bit in bin(e)[3:]:
            res = mul(res,res)
            if bit=="1":
                res = mul(res,b)
        return res
    
    def mul_fib(a,b):
        return (a[0]*b[0]+5*a[1]*b[1])>>1 , (a[0]*b[1]+a[1]*b[0])>>1
    
    def fib_closed(n):
        return pow((1,1),n+1,mul_fib)[1]
    
    def fib_mat(n):
        return pow(np.array([[1,1],[1,0]],'O'),n,op.matmul)[0,0]
    
    def fib_sequential(n):
        t1,t2 = 1,1
        for i in range(n-1):
            t1,t2 = t2,t1+t2
        return t2
    
    def sum_fib_direct(n):
        t1,t2,res = 1,1,1
        for i in range(n):
            t1,t2,res = t2,t1+t2,res+t2
        return res
        
    def sum_fib(n,method="closed"):
        if method == "direct":
            return sum_fib_direct(n)
        return globals()[f"fib_{method}"](n+2)-1
    
    methods = "closed mat sequential direct".split()
    
    def f(method):
        def f(n):
            return sum_fib(n,method)
        f.__name__ = method
        return f
    
    for method in methods:
        B.add_function(method)(f(method))
    
    B.add_arguments('N')(lambda:(2*(1<<k,) for k in range(23)))
    
    r = B.run()
    r.plot()
    
    import matplotlib.pylab as P
    P.savefig(fib.png)
    

    【讨论】:

    • 声称这些方法只提供常数因子优化的说法是错误的。他们只是轻信以天真的方式进行整数乘法的假设。 Python 使用 Karatsuba 算法将大整数相乘。因此,即使从理论上讲,这种说法也是错误的。实际上,对于这个数字范围n=10000,您只有大约 14 乘法(数字或矩阵),最大的数字只有 2K 位。即使是简单的乘法,这些方法也可能比“直接”方法做得更好。
    【解决方案4】:

    我不确定你是如何处理近一秒钟的事情的。这是没有花哨的记忆版本:

    class fibs(object):
        def __init__(self):
            self.thefibs = {0:0, 1:1}
    
        def __call__(self, n):
            if n not in self.thefibs:
                self.thefibs[n] = self(n-1)+self(n-2)
            return self.thefibs[n]
    
    dog = fibs()
    
    sum([dog(i) for i in range(40) if dog(i) < 4000000])
    

    【讨论】:

    • 我不知道如何使用类来解决这些类型的问题,但我一定会研究的。
    猜你喜欢
    • 2021-02-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-28
    • 1970-01-01
    • 1970-01-01
    • 2018-11-09
    相关资源
    最近更新 更多