星星之火OIer:斐波那契数列(二)——O(logn)算法(矩阵加速)

上一讲中,我们已经了解了关于斐波那契数列的O(n)算法

但是我们在上一讲留了一个问题

当n几近于INT_MAX

又该怎么处理呢

那就要引入我们今天讲的::

矩阵加速

这个神奇的玩意可以将时间复杂度降至O(logn)

是不是很神奇!?

下面开始我们的正题

首先,让我们来了解了解矩阵::

A=\begin{bmatrix} a_1_1 & a_1_2 & \cdots & a_1_n\\ a_2_1 & a_2_2 & \cdots & a_2_n\\ \cdots & \cdots & \cdots & \cdots\\ a_m_1 & a_m_2 & \cdots & a_m_n \end{bmatrix}

这个A就是一个m*n的矩阵

说直白点

m*n的矩阵就指的是一个长方形的东西里面有mn列的,有m*n这么多个数

然后呢,矩阵有一些基本的运算

这些基本运算在作者的另一篇博客里有详细的讲解

这里主要提一下矩阵的乘法::

矩阵乘法有一个要求

两个矩阵必须一个为n*m,一个为m*p

即这第一个矩阵的列必须和第二个矩阵的行数相同

就像是这样::

A=\begin{bmatrix} 3 & 5 & -2 \\ 0 & -4 & 7 \end{bmatrix},B=\begin{bmatrix} 3 & 5 & -1 & 0 \\ 0 & -2 & -5 & 2 \\ -1 & 0 & 3 & -4 \end{bmatrix}

然后乘出来应该是一个n*p的矩阵

如上图

A=2*3,B=3*4

所以乘出来的C应该是2*4的矩阵

怎么来的呢??

乘出来的矩阵的第一行的第一个数是由A矩阵的第一行乘B矩阵的第一列

第一行的第二个数是由A矩阵的第一行乘B矩阵的第二列

第二行的第一个数是由A矩阵的第二行乘B矩阵的第一列

n行的第p个数是由A矩阵的第n行乘B矩阵的第p

依次这样下去,就会得到::

A*B=C=\begin{bmatrix} 3 & 5 & -2 \\ 0 & -4 & 7 \end{bmatrix}*\begin{bmatrix} 3 & 5 & -1 & 0 \\ 0 & -2 & -5 & 2 \\ -1 & 0 & 3 & -4 \end{bmatrix}=

\begin{bmatrix} 3*3+5*0+(-2)*(-1) & 3*5+5*(-2)+(-2)*0 & 3*(-1)+5*(-5)+(-2)*3 & 3*0+5*2+(-2)*(-4) \\ 0*3+(-4)*0+7*(-1) & 0*5+(-4)*(-2)+7*0 & 0*(-1)+(-4)*(-5)+7*3 & 0*0+(-4)*2+7*(-4) \end{bmatrix}=

C=\begin{bmatrix} 11 & 5 & -34 & 18 \\ -7 & 8 & 41 & -36 \end{bmatrix}

手算的,有错请帮忙指正一下

而且大家可以看一下,矩阵乘法满足交换律

即::A*B*C=A*(B*C)

大家可以自行推一推

再贴一个矩阵乘法的板子

结构体版::

struct matrix {
    int n,m;
    int mat[105][105];
    matrix() {
        n=0,m=0;
        memset(mat,0,sizeof(mat));
    }
    void READ(const int N,const int M) {
        n=N,m=M;
        for(int i=1;i<=N;i++)
            for(int j=1;j<=M;j++)
                read(mat[i][j]);
    }
    matrix cut(const int row,const int col) const {
        matrix res;
        res=matrix();
        res.n=n-1,res.m=m-1;
        for(int i=1,cnt1=1;i<=n;i++)
            if(i^row) {
                for(int j=1,cnt2=1;j<=m;j++)
                    if(j^col) {
                        res.mat[cnt1][cnt2]=mat[i][j];
                        cnt2++;
                    }
                cnt1++;
            }
        return res;
    }
    const int* operator [](const int row) const {
        return mat[row];
    }
    matrix operator * (const matrix &other) const {
        matrix res;
        res=matrix();
        res.n=n,res.m=other.m;
        for(int i=1;i<=res.n;i++)
            for(int j=1;j<=res.m;j++)
                for(int k=1;k<=m;k++)
                    res.mat[i][j]+=mat[i][k]*other.mat[k][j];
        return res;
    }
    void print() const {
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++)
                if(i==n&&j==m)
                    pr(mat[i][j]);
                else if(j==m)
                    pr(mat[i][j]),putchar('\n');
                else
                    pr(mat[i][j]),putchar(' ');
    }
}a,b,c;

PS:代码里面的read和pr是作者打的快读快输的代码,这里未体现,详情请点击

当然,如果你不会结构体,那就看这个::

for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            for(int k=1;k<=p;k++)
                c[i][k]=c[i][k]+a[i][j]*b[j][k];

简洁明了

然后呢

我们要了解一下快速幂

有这么一个东西

写作x^n

TA还可以写作x^\frac{n}{2}*x^\frac{n}{2}(n为偶数)

于是就有了快速幂

快速幂可以在O(logn)的时间复杂度内算出x^n

贴个快速幂的板子::

int ksm(int b,int n) {//b是底数,n是指数
	int a=1;
	while(n) {
		if(n%2==1)//奇数就多乘一个
			a*=b;
		b*=b;//翻倍
		n/=2;//指数相应就要变为原来的二分之一
	}
	return a;
}

当然你也可以用递归写,只不过时间复杂度要高一些

好像接近我们今天探讨的话题了

有了O(logn)

但矩阵乘法和快速幂有神马用呢??

如果我们能把矩阵和快速幂联系起来,那不就完美了吗

我们设置两个矩阵::

A=\begin{bmatrix} f(1) & f(2) \end{bmatrix},B=\begin{bmatrix} 0 &1 \\ 1 &1 \end{bmatrix}

f(i)还是表示斐波那契的第i

我们用A*B

发现乘出来的C=\begin{bmatrix} f(2) & f(1)+f(2) \end{bmatrix}=C=\begin{bmatrix} f(2) & f(3) \end{bmatrix}

所以只要用A*B*B*B*\cdots *B=A*B^n^-^1就可以得到f(n)

这就是矩阵乘法和快速幂的结合带来的强大效应

当然,如果你不喜欢写成2个数组,你也可以写成这样::

A=\begin{bmatrix} f(2) &f(1) \\ f(1) &f(0) \end{bmatrix}

你把这个数组自乘一下,你会发现跟上面的是一个效果

至于怎么推出来的,请看作者的下一篇博客

然后呢,你就可以浪了

我们只要把快速幂和矩阵加速结合::

PS:这里作者给的是第二种算法,如果想看另一种方法的话,请看作者的另一篇类似的博客

#include
#include
inline void read(long long &x) {
    x=0;
    long long f=1;
    char s=getchar();
    while(s<'0'||s>'9') {
        if(s=='-')
            f=-1;
        s=getchar();
    }
    while(s>='0'&&s<='9') {
        x=x*10+s-48;
        s=getchar();
    }
    x*=f;
}
inline void pr(long long x) {
    if(x<0) {
        putchar('-');
        x=-x;
    }
    if(x>9)
        pr(x/10);
    putchar(x%10+48);
}//快读快输上面有解释
long long A[2][2]={1,1,1,0},n,m,ans[2][2];
inline void cheng(long long a[2][2],long long b[2][2]) {//矩阵乘法运算
    long long c[2][2];
    c[0][0]=c[0][1]=c[1][0]=c[1][1]=0;//乘了之后保存答案
    for(long long i=0;i<2;i++)
        for(long long j=0;j<2;j++)
            for(long long k=0;k<2;k++)
                c[i][j]=(c[i][j]+a[i][k]%m*b[k][j]%m)%m;//按上面说明的乘,记得模
    memcpy(a,c,sizeof(c));//传回答案
}
inline void ksm(long long a[2][2],long long k) {//模拟快速幂
    if(k==1) {//边界
        memcpy(a,A,sizeof(A));
        return;
    }
    long long c[2][2];
    ksm(c,k/2);//这是递归的代码
    cheng(c,c);//平方
    if(k&1)//就是k%2==1即k为奇数
        cheng(c,A);单独再乘一个
    memcpy(a,c,sizeof(c));//传回答案
}
int main() {
    read(n),read(m);
    ksm(ans,n-1);
    pr(ans[0][0]%m);//读入计算输出
}

这就是斐波那契数列的O(logn)算法了

有不对的请帮忙指出哟

谢谢大家啦

另外还有一个

斐波那契数列——番外篇

你可能感兴趣的:(数论)