斐波那契问题的logn解法及推广

1,202 阅读3分钟

引言

斐波那契.001.jpeg

1.斐波那契数列

题目:初始状态 f(1)=1,f(2)=1,f(3)=2,f(4)=3,f(5)=5f(1) = 1,f(2) = 1,f(3) = 2,f(4) = 3,f(5) = 5 ,斐波那契数列问题就是函数的状态转移问题:f(n)=f(n1)+f(n2)f(n) = f(n-1) + f(n - 2)

暴力递归

  • 采用系统压栈的方式完成,时间复杂度为: T(N)=T(N1)+T(N2)+1T(N) = T(N - 1) + T(N-2) + 1 ,复杂度为:O(2N)O(2^N),严格上是:O(1.618N)O(1.618^N)
/**                                      
 * 斐波那契问题:递归解法                           
 * 时间复杂度:O(2^n)                             
 */                                      
public static int fb1(int n) {           
    if (n < 1) return 0;                 
    if (n == 1 || n == 2) return 1;      
    return fb1(n - 1) + fb1(n - 2);      
}                                        

动态规划1.0

  • 很多地方将动态规划,上来搬出的第一个例子就是 斐波那契数列问题,将它的 暴力递归解法动态规划解法进行对比,突显动态规划复杂度上的优势。但是,暴力递归是动态规划的前提,你只有理解了暴力递归的尝试解法,才能顺利成章的推导出它的动态规划解法。
  • 所有的动态规划都是可以由暴力递推推到而来,反之则不一定,它们的包含关系如下:

image-20220416123909478.png

  • 这篇文章不着重讲解暴力递归改动态规划的基本套路,后面会写专门的文章来讲解。
  • 在递归函数 f(n)f(n) 中,只有一个可变参数,对应不同的输入,对应一个固定的输出,是用一张一维表就能装下所有结果。
  • 定义: dp[i]dp[i]表示 f(i)f(i)的结果,那么 dp[i]=dp[i1]+dp[i2]dp[i] = dp[i - 1] + dp[i -2] , 这就是动态规划的转移方程,动态规划的推到过程,就是填写 dpdp 表的过程。
  • 时间复杂度: O(N)O(N),空间复杂度 O(N)O(N)

image-20220416130435729.png

/**                                                              
 * 暴力递归改动态规划,一个可变参数,对应一维dp表                                      
 * 动态规划的过程,本质上就是填写 dp 表的过程                                       
 * 根据递推公式,找到 dp 表的以来关系,将 dp 表填写完成                                
 */                                                              
public static int dp(int n){                                     
    if (n <= 0) return 0;                                        
    if (n <= 2) return 1;                                        
    //1.有暴力递归可知,可变参数取值为[0,n]                                     
    int[] dp = new int[n  + 1];                                  
    //2.下面来填写dp表,先填写初始位置,对应暴力递归的:if (n == 1 || n == 2) return 1; 
    dp[1] = 1;                                                   
    dp[2] = 1;                                                   
    //3.再填写普遍位置                                                  
    for (int i = 3; i <= n ; i++) {                              
        dp[i] = dp[i - 1] + dp[i - 2];                           
    }                                                            
    return dp[n];                                                
}                                                                                                                            

动态规划2.0

  • 在填写 dpdp 表过程中,发现当前位置 ii 只依赖它的第 i1i-1i2i-2 位置,那么只需要用两个变量 dpi1,dp2dpi_1,dp_2 来记录前两个位置,通过滚动更新两个变量就能完成动态规划的过程。省略掉 dpdp 数组。
  • 这个思路就是动态规划中的空间压缩技巧,时间复杂度 O(N)O(N),空间复杂度 O(1)O(1)
  • 所以我们通常看到的斐波那契数列动态规划代码如下,用到空间压缩技巧:
/**                                  
 * 动态规划2.0版本:空间压缩                    
 */                                  
public static int dp2(int n) {       
    if (n <= 0) return 0;            
    if (n <= 2) return 1;            
    int dpi_1 = 1;                   
    int dpi_2 = 1;                   
    for (int i = 3; i <= n; i++) {   
        int temp = dpi_1 + dpi_2;    
        dpi_1 = dpi_2;               
        dpi_2 = temp;                
    }                                
    return dpi_1;                    
}                                    

在后续的动态规划文章中,会重点温习暴力递归改动态规划的基本套路,及空间压缩等技巧。

接下来要讲解的是 斐波那契问题的 log(n)log(n)的解法

2.斐波那契 lognlogn 解法

要解决斐波那契问题 lognlogn 解法,首先需要了解以下几个算法原型:

  • 快速幂,该算法能在 lognlogn 时间复杂度内求解 xnx^n 问题。
  • 线性代数基础,向量与矩阵的乘法问题。

快速幂

  • 求的 xnx^n问题,暴力解是通过将 x乘以x乘以 nn次。事件复杂度为 O(n)O(n)
public static int quickMi(int x,int n){
    //未做输入参数检验                         
    int sum = 1;                       
    while (n != 0){                    
        sum *= x;                      
        n--;                           
    }                                  
    return sum;                        
}                                      

快速幂步骤

  • xnx^n​中,可以将 nn​ 看成二进制的形式,595^9 中,n=9=1001=123+022+021+120n = 9 = 1001 = 1*2^3 + 0 * 2 ^2 + 0* 2^1 + 1 * 2^0
  • 注意 nn 中只有二进制位 11的位置乘积才有结果:

image-20220416151452523.png

  • 那么问题就变成了判断 nn 的二进制表示中,从右往左哪些位置上的值是 11,将 xix^i 乘到结果上去。
public static int quickPow(int x,int n){               
    int sum = 1;                                       
    while (n != 0){                                    
        //二进制中最后一个位置是否为1,是1就乘上去,是 0 就没必要               
        if ((n & 1) == 1){                             
            sum *= x;                                  
        }                                              
        //n无符号右移                                       
        n >>>= 1;                                      
        x *= x;                                        
    }                                                  
    return sum;
}  

斐波那契问题与快速幂

斐波那契数列:f(n)=f(n1)+f(n2)f(n) = f(n - 1) + f(n - 2)

对于一个状态,只有初始态通过前提条件得到状态值,后续的所有状态都不受条件判断的印象,为严格的递推函数,则它有线性代数的解。

先给出结论:这是一个二阶矩阵

  • f(1)=1,f(2)=1f(1) = 1,f(2) = 1
  • f(3),f(2)=f(2),f(1)[abcd]|f(3),f(2)| = |f(2),f(1)| * \begin{gathered} \begin{bmatrix} a & b \\ c & d \end{bmatrix} \end{gathered} => f(4),f(3)=f(3),f(2)[abcd]|f(4),f(3)| = |f(3),f(2)| * \begin{gathered} \begin{bmatrix} a & b \\ c & d \end{bmatrix} \end{gathered}
  • 向量与矩阵的乘积计算,通过初始几个值: f(1)=1,f(2)=1,f(3)=2,f(4)=3f(1) = 1,f(2) = 1,f(3) = 2,f(4) = 3​,就变成求解四元一次方程了,手动计算出该矩阵: [1110] \begin{gathered} \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix} \end{gathered}

image-20220416154400896.png 问题就转变成了求 矩阵 AnA^n 问题,这不就是快速幂么,只是底数变成了一个矩阵。

代码

/**                                                          
 * 斐波那契数列 logn 解法                                            
 * 二阶递推:|a,b|                                                
 *         |c,d|                                             
 */                                                          
public static int fb3(int n) {                               
    if (n < 1) return 0;                                     
    if (n == 1 || n == 2) return 1;                          
    //1.定义二阶矩阵,通过手动计算出来                                      
    int[][] base = {{1, 1}, {1, 0}};                         
    //2.求出 base^(n-2)次方                                      
    int[][] res = matrixPower(base, n - 2);                  
    //3. |f(n),f(n-1)|=|f(2),f(1)|* res = a * f(2) + c * f(1)
    return res[0][0] + res[1][0];                            
}                                                            
                                                             
/**                                                          
 * 矩阵的n次方,快速幂的方法                                             
 */                                                          
public static int[][] matrixPower(int[][] m, int n) {        
    int[][] res = new int[m.length][m[0].length];            
    //1.构造单位矩阵                                               
    for (int i = 0; i < m.length; i++) {                     
        res[i][i] = 1;                                       
    }                                                        
    //2.快速幂求矩阵的n次方                                           
    int[][] temp = m;                                        
    for (; n != 0; n >>= 1) {                                
        //3.如果当前二进制位是1,则相乘                                   
        if ((n & 1) != 0) {                                  
            res = multiMatrix(res, temp);                    
        }                                                    
        temp = multiMatrix(temp, temp);                      
    }                                                        
    return res;                                              
}                                                            
                                                             
/**                                                          
 * 定义两个矩阵的相乘                                                 
 */                                                          
public static int[][] multiMatrix(int[][] m1, int[][] m2) {  
    int[][] res = new int[m1.length][m2[0].length];          
    for (int i = 0; i < m1.length; i++) {                    
        for (int j = 0; j < m2[0].length; j++) {             
            for (int k = 0; k < m2.length; k++) {//列数        
                res[i][j] += m1[i][k] * m2[k][j];            
            }                                                
        }                                                    
    }                                                        
    return res;                                              
}                                                            

3.斐波那契问题推广

对于一个递推公式:f(n)=C1F(n1)+C2f(n2)+...Ckf(nk)f(n) = C_1F(n-1) + C_2 f(n-2) + ... C_kf(n-k) ,其中 C1,C2,....,CkkC_1,C_2,....,C_k,k 都为常数,称为 kk 阶递推式。

  • kk 阶对应的矩阵就是 kk 阶矩阵
  • 以三阶为例:f(n),f(n1),f(n2)=f(3),f(2),f(1)matrixn3次方|f(n),f(n-1),f(n-2)| = |f(3),f(2),f(1)| * matrix的n-3次方

总结:除了初始状态可以根据条件进行设置,后续所有状态都不能根据条件来递推,满足严格递推才能用。

奶牛繁殖问题

开始有一只奶牛,并且保证奶牛永远不会死,每只奶牛过三年后可以生一只奶牛,请问 nn 年后的奶牛数量。

那么该递推公式就是:

  • f(n)=1f(n1)+0f(n2)+1f(n3)f(n) = 1*f(n-1) + 0*f(n-2) + 1*f(n-3)
  • 这是一个三阶问题,对应的矩阵是 333*3

image-20220416160739981.png

/**                                                      
 * 奶牛繁殖问题:                                               
 * 1,2,3,4,6,9                                           
 * f(n) = f(n-1) + f(n-3)                                
 */                                                      
public static int cowPb(int n) {                         
    if (n < 1) return 0;                                 
    if (n == 1 || n == 2 || n == 3) return n;            
    int[][] base = {                                     
            {1, 1, 0},                                   
            {0, 0, 1},                                   
            {1, 0, 0}                                    
    };                                                   
    int[][] res = matrixPower(base, n - 3);              
    return 3 * res[0][0] + 2 * res[1][0] + res[2][0];    
}                                                        

注意

如果题目规定奶牛10年后会死掉,则递推公式变为:

f(n)=1f(n1)+1f(n3)f(n10)f(n) = 1*f(n-1) + 1*f(n-3) - f(n - 10)

贴瓷砖问题

题目描述:给定一块矩阵面积为 n2n*2 ,瓷砖的规格只有 212*1 大小,问有多少种不同的铺砖方法?

递推公式: f(n)=f(n1)+f(n2)f(n) = f(n - 1) + f(n- 2)

image-20220416161920335.png

青蛙跳台阶问题

image-20220416162204852.png

爬楼梯

image-20220416162228872.png

字符达标问题

给定一个数 NN, 设定只由 0011 两种字符,组成的所有长度为 NN 的字符串,如果某个字符串,任何 00 字符的左边都要紧挨着 11,认为这个字符串达标,返回有多少个达标的字符串?

N=6N = 6时候:

[101010],[101011],[101101],[101110],[101111],[110101],[110110],[110111],[111010],[111011],[111101],[111110],[111111][1 0 1 0 1 0], [1 0 1 0 1 1], [1 0 1 1 0 1], [1 0 1 1 1 0], [1 0 1 1 1 1], [1 1 0 1 0 1], [1 1 0 1 1 0], [1 1 0 1 1 1], [1 1 1 0 1 0], [1 1 1 0 1 1], [1 1 1 1 0 1], [1 1 1 1 1 0], [1 1 1 1 1 1]

思路:

  • 1、所有合法的字符都不可能以 00 开头(00 的左边必须紧挨着 11 ),只能以 11 开头, 所以此时就是 n1n-1 的达标数量。
  • 2、开头是 1010 ,剩下的 n2n-2 达标的数量。
  • 3、总的数量即为 f(n)=f(n1)+f(n2)f(n) = f(n - 1) + f(n-2)