我这种小蒟蒻就只能浅谈一下矩阵这种神奇的东西啦。
但正因为是蒟蒻,所以讲的比较好懂(大概)。
本篇分为两部分——>上:矩阵加速递归+下:高斯消元
如果没有你想看的那我深感抱歉...我太弱了只会讲这两个(说不定以后有补充)
——正片开始——
首先,不知道什么是矩阵的这里请:必应
我们用矩阵优化递推一般只需要会矩阵乘法和矩阵加法,矩阵乘法当然用得最多...
既然是浅谈,我就简单讲讲矩阵的这两个基本运算吧。
1. 矩阵加法:
要求:两个矩阵同型(即行列数相同)。然后我们对应位置的元素加起来就好了...
一个N*M的矩阵A加一个N*M的矩阵B会得到一个N*M的矩阵。
2. 矩阵乘法:
要求:两个矩阵中的一个矩阵的列数等于另一个矩阵的行数。
一个N*K的矩阵A和一个K*M的矩阵B相乘会得到一个N*M的矩阵C。
我懒得敲Latex了,自己找去...
好吧,我敲...对于它任意一个元素,值为:
$$C_{i,j}=sum_{k=1}^{n}a_{i,k}b_{k,j} $$
我这是为了美观...
然后就给段代码吧。
struct Matrix{ int a[N][N]; Matrix(){memset(a,0,sizeof a);} int ln,lm;//长度 Matrix operator *(const Matrix&b)const{ Matrix res;res.ln=ln;res.lm=b.lm; for(int i=1;i<=ln;i++) for(int j=1;j<=b.lm;j++) for(int k=1;k<=lm;k++) res.a[i][j]=(res.a[i][j]+a[i][k]*b.a[k][j])%MOD; } };
挺简单的吧?那么它有什么用呢?
用处很多。比如:加速递推,加速递推,……(省略10w+字)。
好啦,just a joke,只是我们只讲加速递推。
你可能会想,一般递推都已经O(n)了,你还要加速?
当然了,一些丧心病狂的出题人超喜欢巨型数据。
比如给你个1e9的递推啥的,都是经常的事了。
想到递推,我就想到今年下半年斐波那契数列。
递推式:$ ext{f(n)=f(n-1)+f(n-2)}$
咳咳,只是为了美观
然后我们来构造递推矩阵。对了,有件事忘说了。
我们考虑使用矩阵乘法加速递推也是有前提的:
决策点的个数少,数据范围大,线性的递推或类线性的递推(以后会讲)
我们注意到,fib只有两个决策点,于是我们尝试使用二维矩阵,不行就加维呗。
然后我们就构造出来了。(告诉我怎么构造的啊?不说锤爆你狗头)
首先,我们构造一个答案矩阵。(???)
看,就是这个:淦,怎么又要用Latex...
$F(n)= left|egin{matrix} f(n) & f(n-1) end{matrix} ight| $
你可能会疑惑,为什么要这么构造?
实际上,我们希望通过F(n)得到F(n+1)来完成一个递推。
我们知道$F(n+1)= left|egin{matrix} f(n+1) & f(n) end{matrix} ight| $
F(n)包含了f(n)和f(n-1)两个信息,正是我们得到f(n+1)所需要的。
那么我们设F(n)*base=F(n+1)。所以我们希望构造出一个base矩阵来满足它的心愿。
由于F(n)*base=F(n+1),所以我们可以设$base= left|egin{matrix} a & b \ c & d end{matrix} ight| $
然后我们需要知道a,b,c,d的值。(为什么base是2*2的啊)
(ans矩阵是1*2的,base矩阵得要是2*2的,才能得到1*2的ans矩阵)。
然后我们先把ans和base乘起来再说:
这篇怎么这么多Latex,点个推荐吧,我太难了TuT
$$left|egin{matrix} f(n) & f(n-1) end{matrix} ight| imes left|egin{matrix} a & b \ c & d end{matrix} ight| = left|egin{matrix} af(n)+cf(n-1) & bf(n)+df(n-1))end{matrix} ight| = left|egin{matrix} f(n+1) & f(n) end{matrix} ight|$$
我们再根据递推式可以得到$base= left|egin{matrix} 1 & 1 \ 1 & 0 end{matrix} ight| $
我们得到了递推矩阵,怎么加速呢?这时我们就要用到强大的快速幂了。
以防有读者不知道快速幂,我这里简单讲解一下,快速幂就是利用二进制划分思想加速幂运算的一个简单算法。
正整数快速幂可以这样写:
ll qpow(int x,int y){ ll tmp=x,con=1; while(y){ if(y&1)con=con*tmp%MOD; tmp=tmp*tmp%MOD; y>>=1; }return con; }
但是这里我们需要矩阵快速幂。
我们简单分析可以得到:$F(n)=F(2) imes base imes base imes ... imes base$,base累乘了n-2次。
为什么是从F(2)开始计算呢?
我们知道$F(2)= left|egin{matrix} f(2) & f(1) end{matrix} ight| $
还知道$F(1)= left|egin{matrix} f(1) & f(0) end{matrix} ight| $,然而我们并不能计算f(0)。
于是我们从F(2)开始计算,矩阵乘法满足结合律,所以我们可以得到最终的结果:
$F(n)= left|egin{matrix} f(2) & f(1) end{matrix}
ight| imes left|egin{matrix} 1 & 1 \ 1 & 0 end{matrix}
ight|$
至此得解,给出代码:
#include<bits/stdc++.h> using namespace std; long long n; const long long mod=1e9+7; struct Matrix{//封装成结构体更方便食用 long long a[3][3]; Matrix(){memset(a,0,sizeof(a));} Matrix operator *(const Matrix &b)const{ Matrix res; for(int i=1;i<=2;i++) for(int j=1;j<=2;j++) for(int k=1;k<=2;k++) res.a[i][j]=(res.a[i][j]+a[i][k]*b.a[k][j])%mod; return res; } }ans,base; void init(){//初始化ans和base矩阵 base.a[1][1]=base.a[1][2]=base.a[2][1]=1; ans.a[1][1]=ans.a[1][2]=1; } void qpow(long long b){//矩阵快速幂 while(b){ if(b&1)ans=ans*base; base=base*base; b>>=1; } } int main(){ scanf("%lld",&n); if(n<=2){ puts("1");return 0; } init(); qpow(n-2); printf("%lld",ans.a[1][1]%mod); return 0; }
斐波那契数列算是最基础的矩阵加速递推的例子了,接下来我们来看看更难一点的。
比如说,我们有这样一个递推式:$f(n)=3 imes f(n-1)+2 imes f(n-2)+f(n-3)+3$
(我为啥要给自己挖这么大一个坑...),这待会儿Latex要写死我
然后我们一起来推一下吧:
首先是答案矩阵!
还是一样,我们设$F(n)=left| egin{matrix}f(n) & f(n-1) & f(n-2) & 3end{matrix} ight|$
记住常数项也需要加一维,那我们的base矩阵是什么样的呢?
推导过程与上面一致,由于要写很长的一个Latex而我又比较懒,所以我就写到纸上了...
于是我们得到了base矩阵:$base= left|egin{matrix} a & b & c & d \ e & f & g & h \ i & j & k & l \ m & n & o & p end{matrix} ight| = left|egin{matrix} 3 & 1 & 0 & 0 \ 2 & 0 & 1 & 0 \ 1 & 0 & 0 & 0 \ 1 & 0 & 0 & 1 end{matrix} ight|$
然后我们写出初始答案矩阵,为了避免出现f(0),我们从F(3)开始计算,得到这样一个行向量:
$F(3)= left| egin{matrix} f(3) & f(2) & f(1) & 3 end{matrix} ight|$
由于从F(3)开始,所以我们算到F(n)实际上只用累成n-3次base,直接qpow(n-3)。
代码在后面,但是在看代码前先尝试自己改一改上面的代码来实现这个递推。
给出初始项:$f_1={3}, f_2={12}, f_3={45}$,检验项(可以自行检查有没有写对):$f_4={165},f_7={7902},f_{10}={377175} $
如果你写对了并且明白了递推矩阵的推导方法,那么你就学会初级的矩阵加速递推了。
难道还有高级的?Bingo。
记得上面说的吗?类线性递推我们也能用矩阵加速。
关于类线性的矩阵加速递推,我会找一个时间补充,若有想看的朋友可以推荐收藏这篇文章不定期来看看。
下篇会讲矩阵&高斯消元...但是我打算先更几篇网络流的blog。(DP高级篇的中和下先咕着...)
update:忘了给代码了,补上
#include<bits/stdc++.h> using namespace std; inline int read(){ int data=0,w=1;char ch=0; while(ch!='-' && (ch<'0'||ch>'9'))ch=getchar(); if(ch=='-')w=-1,ch=getchar(); while(ch>='0' && ch<='9')data=data*10+ch-'0',ch=getchar(); return data*w; } const long long mod=1000000007ll; struct Matrix{ long long a[5][5]; Matrix(){memset(a,0,sizeof a);} Matrix operator *(const Matrix&b)const{ Matrix res; for(int i=1;i<=4;i++) for(int j=1;j<=4;j++) for(int k=1;k<=4;k++) res.a[i][j]=(res.a[i][j]+a[i][k]*b.a[k][j])%mod; return res; } }ans,base; void qpow(long long b){ while(b){ if(b&1)ans=ans*base; base=base*base; b>>=1; } } void init(){ //f[1]=3,f[2]=12,f[3]=45 ans.a[1][1]=45;ans.a[1][2]=12;ans.a[1][3]=3;ans.a[1][4]=3; base.a[1][2]=base.a[2][3]=base.a[3][1]=base.a[4][1]=base.a[4][4]=1; base.a[1][1]=3;base.a[2][1]=2; } int main(){ int n=read(); if(n==1) return puts("3"),0; else if(n==2) return puts("12"),0; else if(n==3) return puts("45"),0; else{ init(); qpow(n-3); printf("%lld",ans.a[1][1]%mod); } return 0; }