矩阵乘法 学习笔记
配合\(0x34\)矩阵乘法食用效果更加
矩阵乘法的规则
\(A[n][m]*B[m][p]=C[n][p]\)
即,参加矩阵乘法运算的第一个矩阵的列数必须必须等于第二个矩阵的行数,所得到的结果矩阵的行数、列数分别等于第一个矩阵的行数、第二个矩阵的列数。
下图展示了\(A*B=C\)
利用矩阵乘法的性质,我们可以得到其主要用途之一:优化线性递推式
\(F\)是\(1*n\)矩阵,\(A\)是\(n*n\)矩阵,则\(F'=F*A=1*n\)矩阵,实现模拟递推
三道例题,理解这个过程。
斐波那契数列
解题突破口:\(Fib[n]=Fib[n-1]+Fib[n-2]\),即\(Fib[n]\)只与\(Fib[n-1]\)和\(Fib[n-2]\)有关
符合所谓线性递推所以我们可以设计出原始矩阵与转移矩阵,从而利用矩阵乘法解决此题
数学作业
暴力方法
res=1;
for(int i=2;i<=n;i++)
{
res*=power(10,numlen(i));
res=(res+i)%m;
}
不难想到,对于每加入的一个数i,\(res=res*(10^numlen(i))+i\),\(numlen(i)\)是i的位数。
这样的递推式依然可以用矩阵乘法进行优化
暴力算法可以理解为\(dp[i]=dp[i-1]*(10^numlen(i))+i\)
定义矩阵\((dp[i-1],i,1)\)通过某转移矩阵相乘,得到\((dp[i],i+1,1)\),第一项即为答案项。
转移矩阵长这样
注意:K非定值,需要改变。
下附AC代码
点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
using namespace std;
struct mat
{
ll a[3][3];
}c,ans;
ll n,m,tmp,t[20];
mat operator *(mat a, mat b)
{
mat c;
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
c.a[i][j]=0;
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
for(int k=0;k<3;k++)
c.a[i][j]=(c.a[i][j]+a.a[i][k]*b.a[k][j])%m;
return c;
}
mat power(mat x, ll y)
{
if(y==1) return x;
mat z=power(x,y/2);
z=z*z;
if(y&1) z=z*x;
return z;
}
void init()
{
t[0]=1;
ans.a[0][1]=1;
ans.a[0][2]=1;
c.a[1][0]=1;
c.a[1][1]=1;
c.a[2][1]=1;
c.a[2][2]=1;
}
int main()
{
scanf("%lld%lld",&n,&m);
init();
for(int i=1;i<=18;i++) t[i]=t[i-1]*10;
for(int i=1;;i++)
{
c.a[0][0]=t[i]%m;
tmp=min(n,t[i]-1)-t[i-1]+1;
ans=ans*power(c,tmp);
if(t[i]-1>=n) break;
}
printf("%lld\n",ans.a[0][0]);
return 0;
}
佳佳的斐波那契
初读此题,就想到了使用矩阵优化递推完成。根据斐波那契数列的性质,产生两种方法,不过都要用矩阵加速。
方法一:结论简化+矩阵优化
part1结论推导
上一串斐波那契数列,\(F_n\)表示斐波那契数列第\(n\)项的值:
\(1,1,2,3,5,8,13,21,34,55...\)
再用\(S_n\)来表示斐波那契数列前\(n\)项的和:
\(1,2,4,7,12,20,33,54...\)
错位放在一起试试看:
1 1 2 3 5 8 13 21 34 55...
1 2 4 7 12 20 33 54...
那么我们的结论就出来了:\(S(n)=F(n+2)-1\)
part2矩阵优化
推式子:
\(T_n\)
\(=F_1+2*F_2+3*F_3+4*F_4+...+n*F_n\)
\(=(F_1+F_2+F_3+F_4...+F_n)+(F_2+F_3+F_4+...F_n)+(F_3+F_4+...F_n)+...+F_n\)
\(=S_n+(S_n-F_1)+(S_n-F_1-F_2)+...+(S_n-F_1-F_2-...-F_{n-1})\)
\(=n*S_n-(S_1+S_2+S_3+...+S_{n-1})\)
\(=n*S_n-(F_3+F_4+F_5+...+F_{n+1}-(n-1))\)//有没有一种在\(F\)和\(S\)之间反复横跳的感觉? 有就对了
\(=n*S_n-S_{n+1}+F_1+F_2+(n+1)\)
由此,\(T_n\)被转化成了\(S_n\),变成了求斐波那契数列求第\(n\)项的问题。
方法二:直接矩阵优化
part2设计矩阵
观察\(T_n\)的定义,我们可以发现\(T_n=T_{n-1}+n*F_n\),以此为突破口设计递推。
设计目标矩阵:首先我们需要记录答案的\(T_n\),然后为了得到\(n*F_n\),我们可以设计\(F_n,F_{n+1},(n+1)F_{n+1},nF_n\),统共五维。
设计原始矩阵:方便推出目标矩阵,目标矩阵的上一个矩阵为\(T_{n-1},F_{n-1},F_n,nF_n,(n-1)F_{n-1}\)。
初始化:五个维度的初始值依次为\(0,0,1,1,0\)。
设计转移矩阵:依照原始矩阵和目标矩阵,我们可以通过以下矩阵实现转移。
AC代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
#define int long long
using namespace std;
int n,m;
void mul(int f[5],int a[5][5])
{
int c[5];
memset(c,0,sizeof(c));
for(int j=0;j<5;++j)
for(int k=0;k<5;++k)
c[j]=(c[j]+(ll)f[k]*a[k][j])%m;
memcpy(f,c,sizeof(c));
}
void mulself(int a[5][5])
{
int c[5][5];
memset(c,0,sizeof(c));
for(int i=0;i<5;++i)
for(int j=0;j<5;++j)
for(int k=0;k<5;++k)
c[i][j]=(c[i][j]+(ll)a[i][k]*a[k][j])%m;
memcpy(a,c,sizeof(c));
}
signed main()
{
scanf("%d%d",&n,&m);
int f[5]={0,0,1,1,0};
int a[5][5]={{1,0,0,0,0},{0,0,1,2,0},{0,1,1,1,0},{1,0,0,1,1},{0,0,0,1,0}};
for(;n;n>>=1)
{
if(n&1) mul(f,a);
mulself(a);
}
printf("%d\n",f[0]);
}
方法总结
1.审题
判断是否为线性递推式,尝试写出暴力的递推表达式;
2.设计矩阵
根据题意,明确递推关系,找到原始和目标矩阵;
3.设计转移
找到连接原始和目标矩阵的转移矩阵;
4.注意细节
遍历顺序,取模运算等等。