LeetCode 0509. 斐波那契数:尝试以四种方式吃透(四种大方法+两种小优化)

164 阅读1分钟

【LetMeFly】尝试以四种方式吃透:509.斐波那契数(四种大方法+两种小优化)

先说明本题解法:

  1. 动态规划(及 原地滚动的优化)
  2. 递归(及 记忆化的优化)
  3. 矩阵快速幂
  4. 通项公式

力扣题目链接:leetcode.cn/problems/fi…

斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 01 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定 n ,请计算 F(n)

 

示例 1:

输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1

示例 2:

输入:n = 3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2

示例 3:

输入:n = 4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3

 

提示:

  • 0 <= n <= 30

方法一:动态规划

开辟一个大小为3030的数组aa(或开辟大小为n+1n+1的数组也可)

初始值a[0]=0,a[1]=1a[0] = 0, a[1] = 1

之后从下标22开始到nn为止,使用转移方程a[n]=a[n1]+a[n2]a[n] = a[n - 1] + a[n - 2]求解

最终返回a[n]即可

  • 时间复杂度O(n)O(n)
  • 空间复杂度O(C)O(C),这里CC是数据中nn的最大值3030(也可以只开辟n+1n+1的数组,则空间复杂度为O(n)O(n)

AC代码

C++

class Solution {  // 开辟一整个数组
public:
    int fib(int n) {
        int a[31] = {0, 1};
        for (int i = 2; i <= n; i++)
            a[i] = a[i - 1] + a[i - 2];
        return a[n];
    }
};

方法一.2:动态规划 + 原地滚动

不难发现,在计算a[n]a[n]时,我们只用到了a[n1]a[n-1]a[n2]a[n-2],再往前的数据就再也用不到了

因此,我们只需要使用两个额外的空间0_01_1来分别记录a[n1]a[n-1]和a[n-2]的值,在计算过程中,不断更新的值,在计算过程中,不断更新_0_1$的值即可

  • 时间复杂度O(n)O(n)
  • 空间复杂度O(1)O(1)

AC代码

C++

class Solution {  // 两个额外变量模拟
public:
    int fib(int n) {
        if (n < 2)
            return n;
        int _0 = 0, _1 = 1;  // 分别代表a[n - 2]和a[n - 1]
        int ans;
        for (int i = 2; i <= n; i++) {
            ans = _0 + _1;
            _0 = _1, _1 = ans;  // Update
        }
        return ans;
    }
};

方法二:递归

斐波那契数列很容易看出“递归”

题目都说明了,F(n)=F(n1)+F(n2)F(n) = F(n - 1) + F(n - 2),终止条件是n=0n = 0n=1n = 1

那么,我们只需要在非终止条件下无脑递归即可。

  • 时间复杂度O(n2)O(n^2)这个时间复杂度待会儿分析
  • 空间复杂度O(n)O(n),不论总递归次数为多少,总递归深度为nn

AC代码

C++

class Solution {  // 递归
public:
    int fib(int n) {
        if (n < 2)
            return n;
        return fib(n - 1) + fib(n - 2);
    }
};

方法二.2:递归 + 记忆化

方法二在数据量小的前提下能很轻松地计算出结果。

但是为什么方法二的时间复杂度是O(n2)O(n^2)呢?

不难发现,计算F(5)F(5)时,会调用F(4)F(4)F(3)F(3),但在计算F(4)F(4)时,会再调用一次F(3)F(3),也就是说F(3)F(3)不只被调用了一次

例如计算F(6)F(6)时:

demo

F(4)F(4)计算了两次,F(3)F(3)计算了三次,F(2)F(2)更是计算了五次。

nn越大,这种重复计算就越明显。

那么,既然算过一遍F(3)F(3)了,为什么要再算一次呢?

记忆化出现了

我们使用一个哈希表,将计算过的结果记录下来,那么再次调用这个函数时,直接返回之前计算过的结果不就可以了吗?

demo2

这样就避免了没必要的重复计算。

  • 时间复杂度O(n)O(n)
  • 空间复杂度O(n)O(n)

AC代码

C++

class Solution {  // 递归 + 记忆化
private:
    unordered_map<int, int> ma;
public:
    int fib(int n) {
        if (n < 2)
            return n;
        if (ma.count(n))
            return ma[n];
        return ma[n] = fib(n - 1) + fib(n - 2);
    }
};

方法三:矩阵快速幂

[1110][anan1]=[an+an1an]=[an+1an]\left[\begin{array}{ll}1 & 1 \\1 & 0\end{array}\right]\left[\begin{array}{c}a_n \\ a_{n-1}\end{array}\right]=\left[\begin{array}{c}a_n+a_{n-1} \\ a_n\end{array}\right]=\left[\begin{array}{c}a_{n+1} \\ a_n\end{array}\right]

因此

[an+1an]=[1110]n[a1a0]\left[\begin{array}{c} a_{n+1} \\ a_n \end{array}\right]=\left[\begin{array}{ll} 1 & 1 \\ 1 & 0 \end{array}\right]^{n}\left[\begin{array}{l} a_1 \\ a_0 \end{array}\right]

因此可以使用矩阵快速幂求出

[1110]n1\left[\begin{array}{ll} 1 & 1 \\ 1 & 0 \end{array}\right]^{n-1}

再将其右乘[a1a0]\left[\begin{array}{c}a_{1} \\ a_0\end{array}\right]即得到[anan1]\left[\begin{array}{c}a_{n} \\ a_{n-1}\end{array}\right]

假设:

[1110]n1=[xymn]\left[\begin{array}{ll}1 & 1 \\ 1 & 0\end{array}\right]^{n-1}=\left[\begin{array}{ll}x & y \\ m & n\end{array}\right]

那么:

[anan1]=[1110]n1[a1a0]=[xymn][10]=[xm]\left[\begin{array}{c}a_{n} \\ a_{n-1}\end{array}\right]=\left[\begin{array}{ll}1 & 1 \\ 1 & 0\end{array}\right]^{n-1}\left[\begin{array}{c}a_{1} \\ a_0\end{array}\right]=\left[\begin{array}{ll}x & y \\ m & n\end{array}\right]\left[\begin{array}{ll}1\\ 0\end{array}\right]=\left[\begin{array}{ll}x\\ m\end{array}\right]

因此an=xa_n=x,也就是[1110]n1\left[\begin{array}{ll}1 & 1 \\ 1 & 0\end{array}\right]^{n-1}的左上角的值

  • 时间复杂度O(logn)O(\log n)
  • 空间复杂度O(1)O(1)

AC代码

C++

struct Matrix {
    int a[2][2];

    Matrix(int x, int y, int m, int n) {
        a[0][0] = x, a[0][1] = y;
        a[1][0] = m, a[1][1] = n;
    }

    Matrix() {
        a[0][0] = 0, a[0][1] = 0;
        a[1][0] = 0, a[1][1] = 0;
    }
};

Matrix operator* (const Matrix& a, const Matrix& b) {
    Matrix result;
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 2; j++) {
            result.a[i][j] = a.a[i][0] * b.a[0][j] + a.a[i][1] * b.a[1][j];
        }
    }
    return result;
}

class Solution {
private:
    Matrix Pow(Matrix a, int n) {  // 这里只接受>0的n
        Matrix result(1, 0, 0, 1);
        while (n) {
            if (n & 1)
                result = result * a;
            n >>= 1;
            a = a * a;
        }
        return result;
    }
public:
    int fib(int n) {
        if (n < 2)
            return n;
        return Pow(Matrix(1, 1, 1, 0), n - 1).a[0][0];
    }
};

方法四:通项公式

使用生成函数求斐波那契的通项公式

    F(x)=F1x+F2x2+F3x3+F4x4+\ \ \ \ F(x)=F_1x+F_2x^2+F_3x^3+F_4x^4+\cdots

  xF(x)=           F1x2+F2x3+F3x4+\ \ xF(x)=\ \ \ \ \ \ \ \ \ \ \ F_1x^2+F_2x^3+F_3x^4+\cdots

x2F(x)=                        F1x3+F2x4+x^2F(x)=\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ F_1x^3+F_2x^4+\cdots

因此,(x2+x)F(x)=F1x2+(F2+F1)x3+(F3+F4)x4+=F1x2+F3x3+F4x4+(x^2+x)F(x)=F_1x^2+(F_2+F_1)x^3+(F_3+F_4)x^4+\cdots=F_1x^2+F_3x^3+F_4x^4+\cdots

因此,(1(x2+x))F(x)=F1x+(F2F1)x2(1-(x^2+x))F(x)=F_1x+(F_2-F_1)x^2

(1xx2)F(x)=x(1-x-x^2)F(x)=x

所以F(x)=x1xx2=x(x512)(x+5+12)=15(1512x+1+15+12x1)F(x)=\frac{x}{1-x-x^2}=-\frac{x}{\left(x-\frac{\sqrt{5}-1}{2}\right)\left(x+\frac{\sqrt{5}+1}{2}\right)}=-\frac{1}{\sqrt{5}}(\frac{1}{\frac{\sqrt{5}-1}{2} x+1}+\frac{1}{\frac{\sqrt{5}+1}{2} x-1})

我们求出了斐波那契数列的生成函数

接下来通过生成函数求原始数列

G(x)=1ax+bG(x)=\frac{1}{ax+b},其对应的原始数列是{gi}\{g_i\}

则有1ax+b=i=1gixi\frac{1}{ax+b}=\sum_{i=1}^\infty g_ix^i

已知nn\to \infty1+x+x2++xn=11x1+x+x^2+\cdots+x^n=\frac{1}{1-x}

x-x替换xx11+x=1x+x2x3+\frac{1}{1+x}=1-x+x^2-x^3+\cdots

所以1bx+b=1b1bx+1bx21bx3+\frac{1}{bx+b}=\frac1b-\frac1bx+\frac1bx^2-\frac1bx^3+\cdots

abx\frac{a}bx替换xx1ax+b=1bab2x+a2b3x2a3b4x3+\frac1{ax+b}=\frac{1}{b}-\frac{a}{b^{2}} x+\frac{a^{2}}{b^{3}} x^{2}-\frac{a^{3}}{b^{4}} x^{3}+\cdots

所以xnx^n的系数gn=(1)nanb(n+1)g_n=(-1)^na^nb^{-(n+1)}

之前我们求出F(x)=15(1512x+1+15+12x1)F(x)=-\frac{1}{\sqrt{5}}(\frac{1}{\frac{\sqrt{5}-1}{2} x+1}+\frac{1}{\frac{\sqrt{5}+1}{2} x-1})

对于1512x+1\frac{1}{\frac{\sqrt{5}-1}{2} x+1},令a=512,b=1a=\frac{\sqrt{5}-1}{2},b=1gn=(152)ng_n=(\frac{1-\sqrt5}{2})^n,因此G(x)={(152)x}G(x)=\{(\frac{1-\sqrt5}{2})^x\}

对于15+12x1\frac{1}{\frac{\sqrt{5}+1}{2} x-1},令a=5+12,b=1a=\frac{\sqrt{5}+1}{2},b=-1gn=(1+52)ng_n=-(\frac{1+\sqrt5}{2})^n,因此G(x)={(1+52)x}G(x)=\{-(\frac{1+\sqrt5}{2})^x\}

所以an=15[(152)x(1+52)x]=15[(1+52)n(152)n]a_n=-\frac{1}{\sqrt{5}}[(\frac{1-\sqrt5}{2})^x-(\frac{1+\sqrt5}{2})^x]=\frac{1}{\sqrt{5}}\left[\left(\frac{1+\sqrt{5}}{2}\right)^{n}-\left(\frac{1-\sqrt{5}}{2}\right)^{n}\right] 即为所求

  • 时间复杂度不好衡量(也不能说O(1)吧,但是肯定比矩阵快速幂快,毕竟CPU有专门的求幂指令)
  • 空间复杂度O(1)O(1)

AC代码

C++

class Solution {
public:
    int fib(int n) {
        return 1. / sqrt(5) * (pow((1 + sqrt(5)) / 2, n) - pow((1 - sqrt(5)) / 2, n));
    }
};

有同学说,比赛的时候忘记了通项公式怎么办,总不能现场手推一遍吧

其实我们只需要记得通项公式的大概形势:an=15[(1+52)n(152)n]=x1y1n+x2y2na_n=\frac{1}{\sqrt{5}}\left[\left(\frac{1+\sqrt{5}}{2}\right)^{n}-\left(\frac{1-\sqrt{5}}{2}\right)^{n}\right]=x_1y_1^n+x_2y_2^n

因此代入几个aa就把x1,y1,x2,y2x_1,y_1,x_2,y_2解出来了(a0=0,a1=1,a_0=0,a_1=1,\cdots

同步发文于CSDN,原创不易,转载请附上原文链接哦~ Tisfy:letmefly.blog.csdn.net/article/det…