浅谈矩阵在OI中的应用

本文作者太菜了,不会用专业的数学术语表达,在本文中用相对通俗的语言进行讲解。

定义

矩阵(Matrix)是一个按照长方阵列排列的复数或实数集合

可以想象为一个 n×m 的二维数组,可以表示为:

A=[a1,1a1,2a1,ma2,1a2,man,1an,m1an,m]

如果 n=m,那么 A 也被称为 n 阶矩阵

单位矩阵 I 表示 Ii,i=1,其余都为 0 的矩阵。

基本运算

加减法表示两个大小相同的矩阵每个对于位置相加。

A+B=[a1,1+b1,1a1,2+b1,2a1,m+b1,ma2,1+b2,1a2,m+b2,man,1+bn,1an,m1+bn,m1an,m+bn,m]

一个矩阵乘上一个数,表示每个位置都乘上这个数。

A×b=[a1,1×ba1,2×ba1,m×ba2,1×ba2,m×ban,1×ban,m1×ban,m×b]

乘法

两个矩阵相乘仅当第一个矩阵 A 的列数和另一个矩阵 B 的行数相等时才能乘。如 Am×n矩阵和 Bn×p 矩阵,它们的乘积 C 是一个 m×p 矩阵。

乘法满足

Ci,j=k=1nAi,k×Bk,j

计该运算为 C=AB

也就是 Ci,j 表示 A 的第 i 行,B 的第 j 列的每一个元素乘起来的和。

Matr operator *(const Matr &x,const Matr &y){
	Matr z;
	memset(z.cnt,0,sizeof(z.cnt));
	for(int k=1;k<=n;++k)
		for(int i=1;i<=n;++i)
			for(int j=1;j<=n;++j)
				z.cnt[i][j]=(z.cnt[i][j]+x.cnt[i][k]*1ll*y.cnt[k][j]%mod)%mod;
	return z;
}

需要注意的是,矩阵乘法满足结合律和分配率,但不满足交换率

因此,矩阵乘法是可以用快速幂的

需要注意的是,对于任意矩阵 AA0=I,所以可以将 I 定义为初值。

【模板】矩阵快速幂

板子题,直接套一个矩阵乘法和快速幂就好了。

时间复杂度 O(n3logk)

#include<bits/stdc++.h>
using namespace std;
const long long N=105;
long long n,k,mod=1E9+7;
struct hzy{long long cnt[N][N];}a;
hzy operator *(const hzy &x,const hzy &y){
	hzy z;
	memset(z.cnt,0,sizeof(z.cnt));
	for(long long k=1;k<=n;++k)
		for(long long i=1;i<=n;++i)
			for(long long j=1;j<=n;++j)
				z.cnt[i][j]=(z.cnt[i][j]+x.cnt[i][k]*y.cnt[k][j]%mod)%mod;
	return z;
}
hzy kuai(hzy a,long long b){
	hzy c;
	for(long long i=1;i<=n;i++)c.cnt[i][i]=1;
	for(;b;b>>=1){
		if(b&1)c=c*a;
		a=a*a;
	}
	return c;
}
int main(){
	scanf("%lld%lld",&n,&k);
	for(long long i=1;i<=n;i++)
		for(long long j=1;j<=n;j++)
			scanf("%lld",&a.cnt[i][j]);
	a=kuai(a,k);
	for(long long i=1;i<=n;i++){
		for(long long j=1;j<=n;j++)printf("%lld ",a.cnt[i][j]);
		puts("");
	}
}//之前写的代码,写的实在太丑了

矩阵快速幂有啥用?

矩阵快速幂的一个重要应用就是优化 dp 转移。

来个例子斐波那契数列

众所周知,斐波那契数列是可以线性递推的,时间复杂度 O(n)

尝试使用矩阵乘法进行优化。

我们发现,fibi 只与 fibi1fi2 有关。

那么我们构造一个矩阵[fibi2fibi1]

我们通过乘法将它变成[fibi1fibi2+fibi1]

容易得到它乘上的矩阵A=[0111]

事实上,我们发现 Ai,j 就表示在上个状态中的 ft1,ift,j 的贡献就是 Ai,j×ft1,i,因此很容易推出。

乘一次,就向后转移一次,根据结合律对后面相同的矩阵进行合并。

容易发现我们要运算的实际上是 [11]×An1,直接矩阵快速幂即可。

时间复杂度 O(logn)

mat a,b;
int main(){
	scanf("%lld",&n);
	a.cnt[1][1]=0;a.cnt[1][2]=1;
	a.cnt[2][1]=1;a.cnt[2][2]=1;
	a=kuai(a,n-1);
	b.cnt[1][1]=1;
	b.cnt[1][2]=1;
	b=b*a;
	printf("%lld",b.cnt[1][1]%mod);
}

关于斐波那契数列的计算,还可以用生成函数优化为 O(1)

练习:【模板】矩阵加速

来个例题[TJOI2017]可乐(数据加强版)

还是类似,考虑原地不动就是一个自环,自爆就见一个虚点,所有的点都连向它。

fi,j 表示 i 时刻最后停留在 j 的方案数。

暴力 dp 过不了,考虑每次转移的本质都相同,直接矩阵快速幂。

转移矩阵实际上就是邻接矩阵(根据上面推出转移矩阵的结论)。

然后直接做就好了。

时间复杂度 O(n3logt)

int main(){
	Matr c;
	memset(c.cnt,0,sizeof(c.cnt));
	int ans=0;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		c.cnt[x][y]=c.cnt[y][x]=1;
	}
	scanf("%d",&k);
	n++;
	for(int i=1;i<=n;i++)c.cnt[i][i]=1;
	for(int i=1;i<n;i++)c.cnt[i][n]=1;
	c=kuai(c,k);
	for(int i=1;i<=n;i++)(ans+=c.cnt[1][i])%=mod;
	printf("%d",ans);
}

仍然考虑上面的这个问题,如果有 Q 次询问,每次询问时刻 t 时的答案怎么做,Q100

如果暴力每次做,时间复杂度 O(Qn3logt),炸了。

容易发现做一次矩阵乘法的时间复杂度是 O(n3) 的,但我们发现答案的矩阵是 1×n 的。

答案的这个矩阵乘上转移矩阵是 O(n2) 的。

设初始矩阵为 P

答案就是 P×At

快速幂拆成 P×A2t0×A2t1×A2t2...×A2tk

根据结合律,变成(...(((P×A2t0)×A2t1)×A2t2)...×A2tk)

每次运算都是 O(n2) 的,开始时先预处理后面要乘的 A2t

时间复杂度 O(Qn2logt+n3logt)

练习:

【XR-1】分块

Hanjo 2

都是推出暴力式子后直接矩阵快速幂的。

定长最短路

考虑矩阵乘法:

Matr operator *(const Matr &x,const Matr &y){
	Matr z;
	memset(z.cnt,0,sizeof(z.cnt));
	for(int k=1;k<=n;++k)
		for(int i=1;i<=n;++i)
			for(int j=1;j<=n;++j)
				z.cnt[i][j]=(z.cnt[i][j]+x.cnt[i][k]*1ll*y.cnt[k][j]%mod)%mod;
	return z;
}

你会发现这个东西和 Floyd 不仅形似而且神似。

改亿改,变成 Floyd 的形式:

Matr operator *(const Matr &x,const Matr &y){
	Matr z;
	memset(z.cnt,0,sizeof(z.cnt));
	for(int k=1;k<=n;++k)
		for(int i=1;i<=n;++i)
			for(int j=1;j<=n;++j)
				z.cnt[i][j]=min(z.cnt[i][j],max(x.cnt[i][k]+y.cnt[k][j]));
	return z;
}

现在变成了我们熟悉的形式,但有什么用呢?

考虑设邻接矩阵为 A,那么根据前面 dp 的思路, AL 表示走 L 步后得到的最短路的邻接矩阵。

来个模板[USACO07NOV]Cow Relays G

和上面说的一模一样,需要注意的是邻接矩阵中 Ai,i=Inf,不然就可以一直待在原地。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1005;
struct hzy{int cnt[N][N];}a;
int k,m,st,en,b[N],x[N],y[N],z[N],id[N],tot,dis[N][N];
hzy operator*(const hzy x,const hzy y){
    hzy z;
    memset(z.cnt,63,sizeof(z.cnt));
    for(int k=1;k<=tot;k++)
        for(int i=1;i<=tot;i++)
            for(int j=1;j<=tot;j++)z.cnt[i][j]=min(z.cnt[i][j],x.cnt[i][k]+y.cnt[k][j]);
    return z;
}
hzy kuai(hzy a,int b){
    hzy ans;
    memset(ans.cnt,63,sizeof(ans.cnt));
    for(int i=1;i<=tot;i++)ans.cnt[i][i]=0;
    for(;b;b>>=1,a=a*a)if(b&1)ans=ans*a;
    return ans;
}
int main() {
    memset(dis,63,sizeof(dis));
    scanf("%d%d%d%d",&k,&m,&st,&en);
    for(int i=1;i<=m;i++){
        scanf("%d%d%d",&z[i],&x[i],&y[i]);
        b[x[i]]=1,b[y[i]]=1;
    }
    for(int i=1;i<=1000;i++)if(b[i])id[i]=++tot;
    for(int i=1;i<=m;i++)x[i]=id[x[i]],y[i]=id[y[i]];
    for(int i=1;i<=m;i++)
        dis[y[i]][x[i]]=dis[x[i]][y[i]]=min(dis[x[i]][y[i]],z[i]);
    for(int i=1;i<=tot;i++)for(int j=1;j<=tot;j++)a.cnt[i][j]=dis[i][j];
    printf("%d",kuai(a,k).cnt[id[en]][id[st]]);
    return 0;
}

如果修改题目条件,变成走至多 L 步的最短路。

直接每个点连一个边权为 0 的自环,那么就相当于可以一直留在原地,就做完了。

再来个例题Good Vertices

和前面几乎一样,还是跑 Flody,只不过修改一下就好了,Ai,j 表示 iL 步到 j 的最小时刻。

mat operator*(mat a,mat b){
    mat ans;
    for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)ans.cnt[i][j]=1e9;
    for(int k=1;k<=n;k++)
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)ans.cnt[i][j]=min(ans.cnt[i][j],max(a.cnt[i][k],b.cnt[k][j]));
    return ans;
}

还有例题[NOI Online #1 入门组] 魔法

n 很小,依然是 Flody,然后考虑一个转移矩阵表示用一次魔法后的邻接矩阵。

考虑使用一次魔法的改变即可。

转移矩阵:

	for(int k=1;k<=n;k++)
		for(int i=1;i<=n;i++)
			for(int j=1;j<=n;j++)
				dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
	for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)p.cnt[i][j]=dis[i][j];
	for(int i=1;i<=m;i++)//考虑每一条边,把他变成负数
		for(int l=1;l<=n;l++)
			for(int r=1;r<=n;r++)
				p.cnt[l][r]=min(p.cnt[l][r],dis[l][x[i]]+dis[y[i]][r]-z[i]);

练习:[NOI2020] 美食家

维护数列

矩阵乘法还可以和数据结构一起食用。

来个题Fibonacci-ish II

考虑没有修改操作,那么直接用莫队,这样就只需要考虑加一个或者删掉一个数就好了。

考虑新加入一个数会让后面的数都向后平移,即 ax×fiby 变成了 ax×fiby+1

那么这个变化就相当于乘上了一个 fib 的矩阵。

那么这个东西就可以直接用线段树维护了。

code

没想到吧,人傻常数大,这个代码 Time limit exceeded on test 26 了。

考虑优化。

其实不需要延迟标记,直接考虑单点修改,只要可以维护两个区间直接的合并就好了。

原来两个区间的和分别是

x1fib1+x2fib2...+xkfibk

xk+1fib1+xk+2fib2...+xk+nfibn

合并成

x1fib1+x2fib2...+xk+nfibk+n

实际上就是右边的乘上 Fibk,因为 n 很小,直接预处理就好了。

code

练习[THUSCH2017] 大魔法师

动态dp

动态动态规划。

还在学,快了。

To be continue。

行列式

定义一个行列式运算 |A|,也可以表示为 det(A)A 是一个 n 阶矩阵。

det(A)=PS(1)f(P)i=1nAi,Pi

P 表示一个排列,S 表示全排列集合,f(P) 表示 P 的逆序对数量。

也就是每行每列选一个数乘起来,再乘一个容斥系数。

暴力求的时间复杂度为 O(n!×n)

来一些性质:

  • 如果 A 是三角矩阵,那么 det(A)=i=1nAi,i,这个比较显然。

  • 交换 A 的两行或者两列,那么 det(A) 取相反数。

证明:考虑两行,取得位置为 Pi,Pj

交换之后实际上就是逆序对中交换了两个数。

讨论一下就可以发现逆序对个数的变化一定是个奇数。

所以行列式的值取相反数。

交换两列也差不多。

  • 如果 A 有两行一模一样,那么 |A|=0

证明:交换这两行,|A|=|A|

  • 基本变换,将矩阵一行上所有的数乘 t,加到另一行上,|A| 不变。

这个也比较简单,加过去,通过乘法分配律拆成 t+1 个矩阵,然后发现后面的矩阵都有两行相等,都是 0 ,所以 |A| 不变。

发现没有,这个变换和高斯消元一模一样。

可以用高斯消元消为一个上三角,把对角线乘起来就好了。

时间复杂度 O(n3)

#define For(x,l,r) for(int x=l;x<=r;x++)
int det(mat a){
    int ans=1,ff=1;
    For(i,1,n){
        int k=i;
        while(!a.cnt[k][i]&&k<=n)k++;
        if(k!=i){ff*=-1;For(j,i,n)swap(a.cnt[i][j],a.cnt[k][j]);}//交换时记得取相反数
        if(!a.cnt[i][i])return 0;
        ans=1ll*ans*a.cnt[i][i]%mod;
        int Inv=kuai(a.cnt[i][i],mod-2);
        For(j,i+1,n){//普通高斯消元
            if(!a.cnt[j][i])continue;
            int t=1ll*Inv*a.cnt[j][i]%mod;
            For(p,i,n)a.cnt[j][p]=(a.cnt[j][p]-1ll*t*a.cnt[i][p]%mod+mod)%mod;
        }
    }
    return (ans*ff+mod)%mod;
}

然后发现这并不能过【模板】行列式求值

p 非质数时,原来的做法会有问题。

考虑中间那个消元过程,就是一个不断减掉的过程,可以辗转相减。

#define For(i,l,r) for(int i=l;i<=r;i++)
int det(mat a){
    int ans=1,ff=1;
    For(i,1,n)For(j,i+1,n){
        while(a.cnt[i][i]){
            int div=a.cnt[j][i]/a.cnt[i][i];
            For(k,i,n)a.cnt[j][k]=(a.cnt[j][k]-1ll*div*a.cnt[i][k]%mod+mod)%mod;
            ff=-ff,swap(a.cnt[i],a.cnt[j]);
        }
        ff=-ff,swap(a.cnt[i],a.cnt[j]);
    }
    for(int i=1;i<=n;i++)ans=1ll*ans*a.cnt[i][i]%mod;
    return  (ans*ff+mod)%mod;
}

时间复杂度仍然是 O(n3)

  • 范德蒙德行列式

Dn=[111x1xnx1n1xn1n1xnn1]=1i<jn(xjxi)

考虑证明,使用归纳法,对于 n=2,3,可以直接暴力展开证明。

如果 n1 成立,考虑对于 n 阶矩阵,把第 i1 行每个数乘上 x1,然后让第 i 行去减掉,倒着做一遍就可以得到:

Dn=[1110xnx10(xn1x1)xn1n2(xnx1)xnn2]=i=1n1(xnxi)Dn1=1i<jn(xjxi)

LGV引理

先来个模板:[NOI2021] 路径交点

这个引理主要解决有向无环图中不相交路径求和问题。

已知一张有向无环图。

M=[f1,1f1,2f1,mf2,1f2,mfn,1fn,m1fn,m]

fi,j 表示 i 走到 j 的方案数。

M=

杨氏矩阵

还在学,快了。

To be continue。

posted @   houzhiyuan  阅读(460)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
点击右上角即可分享
微信分享提示
主题色彩