升维打击——算法问题的维度碾压

摄影:产品经理 吃:kingname & 产品经理 在小说《三体》里面,我们知道一个词叫做降维打击,通过把对手所在空间的维度降低从而实现团灭整个星系。

但是如果对方所在的维度已经是一维了,降不动了,那么要实现维度打击的办法就是把自己的维度提升。

今天我们将会从二维的层面来解决一维的问题,把时间复杂度从O(n)降低到 O(logn)。

想必大家对斐波拉契数列已经很熟悉了:


1, 1, 2, 3, 5, 8, 13, 21, 34, 55……

从第三位开始,每一位都是前面两位之和。

在一维层面,我们能实现的最快的解法时间复杂度为O(n):


def fib(n):
    if n < 1:
        return 0
    if n in [1, 2]:
        return 1
    a = fib(1)
    b = fib(2)
    for i in range(3, n + 1):
        a, b = b, a + b
    return b

运行效果如下图所示:

知道了第1个数和第2个数,才能计算第3个数。知道了第2个数和第3个数,才能计算第4个数。时间复杂度为 O(n)。前面的数必需都计算出来,才能计算后面的数。要知道第n位数字,就需要知道第 n - 1 位数字,要知道第 n-1 位数字,就需要知道 n-2 位数字。这不是显而易见的吗?

难道还有办法在不计算第3个数的情况下,就把第4个数算出来?

斐波拉契数列是一个一维的数列,看起来就像是一条线一样。你要走到第4个数,必需先走到第3个数。

但如果你从二维的视角来看待,你就会发现实际上你可以从旁边绕过去。

现在我们假设斐波拉契数列第 n 位的值为。第 n-1位的值为。那么我们可以把它表示为一个2行1列的矩阵:

由于,所以上面的矩阵可以转换为:

再进一步转换为:

我们来复习一下矩阵的乘法:

所以

所以

同理,

所以一路推下来:

虽然斐波那契数列没有第0个数,但是我们通过它的生成规则,可以人工设定。

于是,求斐波拉契数列第 n 位的值转换为矩阵运算

运算结果是一个2行1列的矩阵,第1行第1列这个数就是我们需要的结果。

到目前为止,矩阵运算:

看起来还是要乘n 次,时间复杂度还是 O(n)。那么优化在哪里呢?此时我们就要说到快速幂运算了。

以计算为例,看起来要进行15次乘法:


2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 * * 2 * 2 * 2 * 2 * 2 * 2

但实际上,我们真正运算的时候是这样的:

所以,要计算我们可以这样写代码:


a = 2 * 2
b = a * a
c = b * b
result = c * c

所以对于,我们最多只需要计算次乘法即可解决问题(n 为偶数不加1,为奇数加1)。

这种计算方法对矩阵同样适用。并且,对于矩阵计算,Python 的 numpy 库有直接可用的函数,所以要计算一个矩阵的 n 次幂,代码非常简单:


from numpy.linalg import matrix_power
matrix_power(矩阵, n)

所以,我们使用矩阵运算来写一个求斐波那契数列的函数:


import numpy as np
from numpy.linalg import matrix_power

def fib_matrix(n):
    if n == 1:
        return 1
    matrix = np.matrix('1 1; 1 0', dtype='uint64')
    power = matrix_power(matrix, n - 1)
    base_matrix = np.matrix('1; 0')
    result_matrix = np.matmul(power, base_matrix)
    return result_matrix.item(0)

运行效果如下图所示:

但是,由于 numpy 中对整型数字的精度有限定,超出精度以后就会出现数值溢出,变成负数的情况。对于这个问题,我们将会在下一篇文章中介绍解决办法。