我的博客
  • 用矩阵计算斐波那契数列

用矩阵计算斐波那契数列

思路来自于知乎文章

我们都知道斐波那契数列存在递推关系 an=an−1+an−2a_{n}=a_{n-1}+a_{n-2}an​=an−1​+an−2​ 所以写出计算第N项的值,用递归很容易实现

def fibo(n):
    if n <= 2:
        return 1
    return fibo(n-1) + fibo(n-2)

但是要这么写,计算nnn的值稍微大一些的时候就有可能爆栈了。 如果不用递归,我们考虑迭代的方法,也非常直接

def fibo(n):
    cur = 1
    prev = 1
    if n <= 2:
        return cur
    for i in range(2, n):
        res = cur + prev
        prev = cur
        cur = res
    return cur

但是如果我们执意要用递归呢?如果Python支持的话,还有尾递归这个选项。

def fibo(n):
    def __fibo(n, prev, cur):
        if n <= 2:
            return cur
        return __fibo(n-1, cur, cur+prev)
    return __fibo(n, 1, 1)

尾递归的形式很像迭代的写法,只不过,将循环演化成了对自身的调用,cur和 prev这两个值放在了递归调用的参数上。 除了以上三种常规写法,还有使用矩阵进行运算的,类似动态规划的算法。 我们可以把递推关系推广到矩阵形式

(anan−1)=A⋅(an−1an−2)\begin{pmatrix} a_{n} \\ a_{n-1} \end{pmatrix} = A \cdot \begin{pmatrix} a_{n-1} \\ a_{n-2} \end{pmatrix} (an​an−1​​)=A⋅(an−1​an−2​​)

容易得到AAA是一个2×22\times22×2的矩阵,通过递推关系,可以得到

(an−1+an−2an−1)=A⋅(an−1an−2)\begin{pmatrix} a_{n-1} + a_{n-2} \\ a_{n-1} \end{pmatrix} = A \cdot \begin{pmatrix} a_{n-1} \\ a_{n-2} \end{pmatrix} (an−1​+an−2​an−1​​)=A⋅(an−1​an−2​​)

那么AAA的值就可以求得

A=(1110)A = \begin{pmatrix} 1 & 1\\ 1 & 0 \end{pmatrix} A=(11​10​)

矩阵形式的递推公式就是

(anan−1)=(1110)⋅(an−1an−2)\begin{pmatrix} a_{n} \\ a_{n-1} \end{pmatrix} = \begin{pmatrix} 1 & 1\\ 1 & 0 \end{pmatrix} \cdot \begin{pmatrix} a_{n-1} \\ a_{n-2} \end{pmatrix} (an​an−1​​)=(11​10​)⋅(an−1​an−2​​)

如果我们从头开始算起,那么递推公式可以表示成

(anan−1)=(1110)n−1⋅(a1a0)\begin{pmatrix} a_{n} \\ a_{n-1} \end{pmatrix} = \begin{pmatrix} 1 & 1\\ 1 & 0 \end{pmatrix}^{n-1} \cdot \begin{pmatrix} a_{1} \\ a_{0} \end{pmatrix} (an​an−1​​)=(11​10​)n−1⋅(a1​a0​​)

其中(a1a0)T\begin{pmatrix}a_{1} & a_{0}\end{pmatrix}^T(a1​​a0​​)T应该是(10)T\begin{pmatrix}1 & 0\end{pmatrix}^T(1​0​)T 使用 numpy来在 python中处理矩阵的乘法

import numpy as np
def fibo(n):
    matrix = np.array([[1, 1], [1, 0]])
    origin = np.array([[1], [0]])
    for i in range(n):
        origin = np.dot(matrix, origin)
    return origin[0][0]

矩阵的乘法,可以使用快速幂算法来减小时间复杂度 快速幂可以把复杂度将为O(log2n)O(log_{2}n)O(log2​n)。 假设我们想要计算373^{7}37,我们需要计算3×3×3×3×3×3×33\times3\times3\times3\times3\times3\times33×3×3×3×3×3×3,需要计算777次 但是转换一下思路,我们可以根据结合律计算36×33^{6}\times336×3,而 363^{6}36又可以写成32×32×323^2\times3^2\times3^232×32×32需要计算4次。可以看到,当nnn比较大的时候,这样做减少计算过程。推广一下,当计算ana^nan时,得到递推公式

an={an/2×an/2n是偶数a(n−1)2×a(n−1)2×an是奇数1na^n = \begin{cases}a^{n/2} \times a^{n/2} & \text n是偶数 \\ a^{\frac{(n-1)}{2}} \times a^{\frac{(n-1)}{2}} \times a & \text n是奇数 \\ 1 & \text n \end{cases} an=⎩⎨⎧​an/2×an/2a2(n−1)​×a2(n−1)​×a1​n是偶数n是奇数n​

因此我们可以根据它来写出Python递归函数计算ana^nan

def fast_pow(a, n):
    if n <= 0:
        return 1
    if n % 2 == 0:
        return fast_pow(a, n/2) * fast_pow(a, n/2)
    return a * fast_pow(a, (n-1)/2) * fast_pow(a, (n-1)/2)

进一步考虑迭代的做法,37=32×34×313^7=3^2 \times 3^4 \times 3^137=32×34×31,从二进制的角度可以看到3(111)2=320×321×3223^{(111)_{2}}=3^{2^{0}} \times 3^{2^{1}} \times 3^{2^{2}}3(111)2​=320×321×322

也就是说,如果用二进制表示n=(t1t2t3⋯tn)2n = (t_1 t_2 t_3 \cdots t_n)_2n=(t1​t2​t3​⋯tn​)2​ 那么an=∏i=0nati×2ia^n=\prod_{i=0}^n a^{t_i \times 2^i}an=∏i=0n​ati​×2i

代码如下

def fast_pow(a, n):
    res = 1
    while n > 0:
        if n & 1 != 0:
            res = res * a
        a *= a
        n = n // 2
    return res

尾递归的代码也可以根据迭代的思路写出来

def fast_pow(a, n):
    def __fast_pow(res, a,  n):
        if n <= 0:
            return res
        if n & 1 == 0:
            return __fast_pow(res, a*a, n>>1)
        return __fast_pow(res*a, a*a, n>>1)
    return __fast_pow(1, a, n)

不过以上都是针对整数的快速幂的应用,那么该如何用快速幂来求解矩阵呢?并没有太大的区别,只不过将参与运算的整数和运算符号替换成矩阵的就可以了。

python代码如下

import numpy as np


def fast_pow(origin_matrix, n):
    def __fast_pow(res, matrix, n):
        if n <= 0:
            return res
        if n & 1 == 0:
            return __fast_pow(res, np.dot(matrix, matrix), n >> 1)
        return __fast_pow(np.dot(res, matrix), np.dot(matrix, matrix), n >> 1)
    unit_matrix = np.array([[1, 0], [0, 1]])
    return __fast_pow(unit_matrix, origin_matrix, n)


def fibo(n):
    matrix = np.array([[1, 1], [1, 0]])
    origin = np.array([[1], [0]])
    matrix = fast_pow(matrix, n-1)
    origin = np.dot(matrix, origin)
    return origin[0][0]

这里使用的尾递归的方式来计算快速幂,写的稍微好看一点

def fast_pow(times):
    def __fast_pow(init, el, n):
        def u_fast_pow(res, el, n):
            if n <= 0:
                return res
            if n & 1 == 0:
                return u_fast_pow(res, times(el, el), n >> 1)
            return u_fast_pow(times(res, el), times(el, el), n >> 1)

        return u_fast_pow(init, el, n)
    return __fast_pow




def fibo(n):
    matrix = np.array([[1, 1], [1, 0]])
    origin = np.array([[1], [0]])
    unit_matrix = np.array([[1, 0], [0, 1]])
    matrix = fast_pow(np.dot)(unit_matrix, matrix, n-1)
    origin = np.dot(matrix, origin)
    return origin[0][0]
最近更新: 2026/3/15 14:17
Contributors: Keyang Li