矩阵快速幂小结

版权声明:本文为博主原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接和本声明。

矩阵快速幂(大佬们挑不懂的食用)

  • 0 前置
    • 0.1 什么是矩阵
    • 0.2 矩阵の运算
      • 0.2.1 加法
      • 0.2.2 减法
      • 0.2.3 乘法
        • 1 分A
        • 2 旋转
        • 3 分B
        • 4 计算
        • 5 code(敲黑板!!!)
    • 1 矩阵快速幂
      • 1.1 本质
      • 1.2 举个栗子
    • 2 例题(不定时更新)
      • 2.1 Luogu P1962 斐波那契数列
      • 2.2 Luogu P1939 【模板】矩阵加速(数列)
        • 一次构造
        • 二次构造
        • 构造操作
      • 2.3 Vijos 1067 Warcraft III 守望者的烦恼
        • 一次构造
        • 构造操作
      • 2.4 等比数列求和
        • 一次构造
        • 构造操作
  • 3 小结
    • 3.1 难点
    • 3.2 常见问题
    • 3.3 建议
  • 4 致谢

0 前置


0.1 什么是矩阵

"矩阵(Matrix)是一个按照长方阵列排列的复数或实数集合”
—— 摘自百度百科

一看就不是人话
其实它就是个二维数组

0.2 矩阵の运算

A想直接看乘法的巨佬请自动跳到0.2.3(蒟蒻搞不来锚点)

  • 0.2.1 加法

最基础啦
C = A + B ⟷ C i , j = A i , j + B i , j C=A+B\longleftrightarrow C_{i,j}=A_{i,j}+B_{i,j} C=A+BCi,j=Ai,j+Bi,j
举个栗子
[ 1 2 3 4 5 6 7 8 9 ] + [ 10 11 12 13 14 15 16 17 18 ] = [ 11 13 15 17 19 21 23 25 27 ] \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix} + \begin{bmatrix} 10 & 11 & 12 \\ 13 & 14 & 15 \\ 16 & 17 & 18 \\ \end{bmatrix} \text{=} \begin{bmatrix} 11 & 13 & 15 \\ 17 & 19 & 21 \\ 23 & 25 & 27 \\ \end{bmatrix} 147258369+101316111417121518=111723131925152127
满足交换律和结合律,即 A + B = B + A A+B=B+A A+B=B+A A + B + C = A + ( B + C ) A+B+C=A+(B+C) A+B+C=A+(B+C)

  • 0.2.2 减法

也很基础
和整数运算一样哒, A − B = A + ( − B ) A-B=A+(-B) AB=A+(B)
即 \text{即}
C = A − B ⟷ C i , j = A i , j − B i , j C=A-B\longleftrightarrow C_{i,j}=A_{i,j}-B_{i,j} C=ABCi,j=Ai,jBi,j


  • 0.2.3 乘法

重点来啦!
先敲黑板
不满足交换率!
不满足交换率!!
不满足交换率!!!
至于为什么,我们先看它的定义

两个矩阵的乘法仅当第一个矩阵A的列数和另一个矩阵B的行数相等时才能定义。如A是m×n矩阵和B是n×p矩阵,它们的乘积C是一个m×p矩阵 ,它的一个元素: c i , j = a i , j b 1 , j + a i , 2 b 2 , j + ⋯ + a i , n b n , j = ∑ r = 1 n a i , r b r , j c_{i,j}=a_{i,j}b_{1,j}+a_{i,2}b_{2,j}+\cdots+a_{i,n}b_{n,j}=\sum_{r=1}^na_{i,r}b_{r,j} ci,j=ai,jb1,j+ai,2b2,j++ai,nbn,j=r=1nai,rbr,j
—— 摘自百度百科

什么鬼
通俗地来讲,矩阵乘法分这几步(假设 A ⋅ B = C A\cdot B=C AB=C)

1 分A

我们把 A A A每行分开
e . g . e.g. e.g.
[ 1 2 3 4 5 6 7 8 9 ] → [ 1 2 3 ] , [ 4 5 6 ] , [ 7 8 9 ] \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix} \to [1\quad 2\quad 3],[4\quad 5\quad 6],[7\quad 8\quad 9] 147258369[123],[456],[789]

2 旋转

把分出来的行转成竖着的列
e . g . e.g. e.g.
[ 1 2 3 ] → [ 1 2 3 ] [1\quad 2\quad 3]\to \begin{bmatrix} 1\\2\\3 \end{bmatrix} [123]123

3 分B

B B B每一列都分出来(同 A A A

4 计算

经过上面3步, A , B A,B A,B都变成了几个列,然后把这几列对应的位置乘起来再相加
e . g . e.g. e.g.
[ 1 2 3 ] ⋅ [ 6 5 4 ] → 1 ⋅ 6 + 2 ⋅ 5 + 3 ⋅ 4 = 28 \begin{bmatrix}1\\2\\3\end{bmatrix}\cdot\begin{bmatrix}6\\5\\4\end{bmatrix}\to 1\cdot6+2\cdot5+3\cdot4=28 12365416+25+34=28
对于 A A A的第 i i i列和 B B B的第 j j j列,假设经过如上计算得到了 S S S
那么……
C i , j C_{i,j} Ci,j就等于 S S S啦!
e . g e.g e.g
A = [ 1 2 3 3 ] , B = [ 5 4 4 2 3 3 ] , 那么 A=\begin{bmatrix}1&2\\3&3\end{bmatrix},B=\begin{bmatrix}5&4&4\\2&3&3\end{bmatrix},\text{那么} A=[1323],B=[524343],那么
C 1 , 1 = 1 ⋅ 5 + 2 ⋅ 2 = 9 C_{1,1}=1\cdot5+2\cdot2=9 C1,1=15+22=9
C 1 , 2 = 1 ⋅ 4 + 2 ⋅ 3 = 10 C_{1,2}=1\cdot4+2\cdot3=10 C1,2=14+23=10
C 1 , 3 = 1 ⋅ 4 + 2 ⋅ 3 = 10 C_{1,3}=1\cdot4+2\cdot3=10 C1,3=14+23=10
C 2 , 1 = 3 ⋅ 5 + 3 ⋅ 2 = 21 C_{2,1}=3\cdot5+3\cdot2=21 C2,1=35+32=21
C 2 , 2 = 3 ⋅ 4 + 3 ⋅ 3 = 21 C_{2,2}=3\cdot4+3\cdot3=21 C2,2=34+33=21
C 2 , 3 = 3 ⋅ 4 + 3 ⋅ 3 = 21 C_{2,3}=3\cdot4+3\cdot3=21 C2,3=34+33=21
∴ C = [ 9 10 10 21 21 21 ] \therefore C=\begin{bmatrix}9&10&10\\21&21&21\end{bmatrix} C=[92110211021]
其实这个过程和定义里写的是等价哒
毕竟百度百科说的都不是人话
某巨佬 w b k wbk wbk:诶等等,如果进行第4步的时候两列的长度不一样怎么办?比如 [ 1 2 ] \begin{bmatrix}1\\2\end{bmatrix} [12] [ 1 2 3 ] \begin{bmatrix}1\\2\\3\end{bmatrix} 123

别急,仔细看矩阵乘法的定义

两个矩阵的乘法仅当第一个矩阵A的列数和另一个矩阵B的行数相等时才能定义

所以矩阵乘法是有前提哒,这样计算才不会出问题(从这儿就能看出为什么不满足交换率,毕竟换个顺序可能就算不了了)

同时注意到若 A A A a a a行, B B B b b b列,聪明的您肯定很快就会发现 C C C a a a b b b列的矩阵,便可以确定 C C C矩阵的大小。也就是说若 C = A ⋅ B C=A\cdot B C=AB,那么 C C C的行的数量不会超过 A A A,且列的数量不会超过 B B B,所以开数组不需要开额外的大小。

5 code(敲黑板!!!)

能背就背

模板这种东西最好一开始就找一种自己看着舒服的,然后一直写这一种,尽量不要中途更换写法,否则…
—— 某曾换过码风的巨佬的惨案

下面附上本蒟蒻重载运算符的代码(重载运算符后快速幂直接套,比较方便),仅供参考

int n;                                            //矩阵的大小
struct Matrix{                                  
	ll a[MAXN][MAXN];							  //大小视题而定
	Matrix(){memset(a,0,sizeof a);}               //构造函数(不知道什么是构造函数的童鞋们建议学一下,您们可以把初始化的内容放到里面,很方便)
	void Out(){                                   //查错用的(输出矩阵)
		for(register int i=1; i<=n; i++){		 
			for(register int j=1; j<=m; j++)
				printf("%-5lld\n",a[i][j]);
			printf("\n");
		}
	}
	Matrix operator *(Matrix k){                        //重载运算符
		Matrix res;										//这里不写上面那个构造函数的话记得手动初始化一下
		//memset(res.a,0,sizeof(res.a));                //手动初始化
		for(register int i=1; i<=n; i++)          		//枚举A的每一行
			for(register int j=1; j<=n; j++)    		//枚举B的每一列
				for(register int l=1; l<=n; l++)  		//计算
					res.a[i][j] = (res.a[i][j]+a[i][l]*k.a[l][j]%MOD)%MOD;
		return res;
	}
}E;
int main(){
	for(register int i=1; i<=n; i++) E[i][i]=1; 		//构造原始矩阵,相当于整数中的1
}

1 矩阵快速幂

1.1 本质

快速幂其实是用来优化乘法的,我们直接看矩阵乘法是拿来优化什么的

矩阵乘法是一种用来优化递推式的东西
—— 某巨佬xinyue

我们观察一下矩阵乘法的计算方式,在 A ⋅ B = C A\cdot B=C AB=C中,对于 A i , j A_{i,j} Ai,j,它会分别乘以 B B B的第 j j j行的每一项 B j , k B_{j,k} Bj,k,然后分别累加到 C i , k C_{i,k} Ci,k(代码意译)

重点来了
如果 A A A只有一行……
那么根据矩阵乘法的性质……
诶, C C C好像也只有一行,而且和 A A A有一样的列数
于是我们便可以将 A A A C C C从两个矩阵(二维数组)变成两个长度相同的序列(一维数组)

再看运算过程,用之前的方法,聪明的您会很快地发现经过操作后的 A A A只有一列,也就是说对于 C C C中的每一项 C i C_i Ci C i = A 1 ⋅ B 1 , i + A 2 ⋅ B 2 , i + … A n ⋅ B n , i C_i=A_1\cdot B_{1,i}+A_2\cdot B_{2,i}+…A_n\cdot B_{n,i} Ci=A1B1,i+A2B2,i+AnBn,i

注意到 A A A的每一项和 B B B的行数在运算过程中一直是没变的,在变的只有 B B B的列数
于是我们还是采用之前的运算方法,把 B B B的每一列提出来,然后…
我们把提出来的每一个序列 i : ( b 1 , b 2 , … , b n ) i:(b_1,b_2,…,b_n) i:(b1,b2,,bn)抽象成一个操作,把对应的 c ( 即 C i ) c(\text{即}C_i) c(Ci)抽象成结果,那么聪明的您会很快地发现:
矩阵的运算本质就是对一个序列 A A A进行一堆操作并分别保存所有结果
我们要做的就是构造出初始序列 A A A(一般会告诉你)和操作矩阵 B B B,套用快速幂优化矩阵乘法,求出答案


1.2 举个栗子

著名的斐波那契数列

已知 f 1 = 1 , f 2 = 1 , f n = f n − 1 + f n − 2 f_1=1,f_2=1,f_n=f_{n-1}+f_{n-2} f1=1,f2=1,fn=fn1+fn2,求 f n f_n fn

首先写出递推式, f n = f n − 1 + f n − 2 f_n=f_{n-1}+f_{n-2} fn=fn1+fn2

然后构造出初始序列 A A A,注意到推出 f n f_n fn需要知道 f n − 1 , f n − 2 f_{n-1},f_{n-2} fn1,fn2
∴ A \therefore A A要包含 f n − 1 f_{n-1} fn1 f n − 2 f_{n-2} fn2
∴ A = [ f n − 1 f n − 2 … ] ( 省略号指后面可能还要保存其它东西 ) \therefore A=[f_{n-1}\quad f_{n-2}\quad …](\text{省略号指后面可能还要保存其它东西}) A=[fn1fn2](省略号指后面可能还要保存其它东西)
注意到 A ′ = [ f n f n − 1 … ] A'=[f_n\quad f_{n-1}\quad …] A=[fnfn1],其中 f n = f n − 1 + f n − 2                        f n − 1 = f n − 1 f_n=f_{n-1}+f_{n-2}\;\;\;\;\;\;\;\;\;\;\;f_{n-1}=f_{n-1} fn=fn1+fn2fn1=fn1
∴ \therefore 只保存两项 f n − 1 , f n − 2 f_{n-1},f_{n-2} fn1,fn2便可以求出下一个 A ′ A' A
那么 A A A就构造好了: A = [ f n − 1          f n − 2 ] A=[f_{n-1}\;\;\;\;f_{n-2}] A=[fn1fn2]

然后构造操作矩阵 B B B
先写出由 A A A推出 A ′ A' A的过程:
A ′ = [ f n − 1 + f n − 2 f n − 1 ] = [ A 1 + A 2 A 1 ] A'=[f_{n-1}+f_{n-2}\quad f_{n-1}]=[A_1+A_2\quad A_1] A=[fn1+fn2fn1]=[A1+A2A1]
考虑结果序列 C C C的每一项要对应 A ′ A' A,稍微变一下形
C = A ′ = [ A 1 ⋅ 1 + A 2 ⋅ 1 A 1 ⋅ 1 + A 2 ⋅ 0 ] C=A'=[A_1\cdot1+A_2\cdot1\quad A_1\cdot1+A_2\cdot0] C=A=[A11+A21A11+A20]
聪明的您观察系数便可以很快地发现操作序列分别为:
[ 1 1 ] 和 [ 1 0 ] [1\quad 1]\text{和}[1\quad 0] [11][10]
所以便很快地构造出操作矩阵 B : B: B:
B = [ 1 1 1 0 ] B=\begin{bmatrix}1&1\\1&0\end{bmatrix} B=[1110]

先把结果写出来: f n = f n − 1 + f n − 2 f_n=f_{n-1}+f_{n-2} fn=fn1+fn2
同时
[ f n − 1 f n − 2 ] = [ f n − 2 f n − 3 ] ⋅ B = ⋯ = [ f 2 f 1 ] ⋅ B n − 3 = A ⋅ B n − 3 [f_{n-1}\quad f_{n-2}]=[f_{n-2}\quad f_{n-3}]\cdot B=\cdots=[f_2\quad f_1]\cdot B^{n-3}=A\cdot B^{n-3} [fn1fn2]=[fn2fn3]B==[f2f1]Bn3=ABn3
然后就可以很愉快地用矩阵乘法快速幂求出 A ⋅ B n − 3 A\cdot B^{n-3} ABn3啦!
代码见下面例题

2 例题(不定时更新)

2.1 Luogu P1962 斐波那契数列

题目链接
分析略
C o d e Code Code:

#include
using namespace std;

template<typename T>
inline void Read(T &n){
	char ch;bool flag=0;
	while(!isdigit(ch=getchar()))if(ch=='-')flag=1;
	for(n=ch^48;isdigit(ch=getchar());n=(n<<1)+(n<<3)+(ch^48));
	if(flag)n=-n;
}

typedef long long ll;
const ll MOD = 1000000007;

struct Matrix{
	ll a[3][3];
	Matrix(){memset(a,0,sizeof a);}
	void Out(){
		for(register int i=1; i<=2; i++){
			for(register int j=1; j<=2; j++)
				printf("%-5lld\n",a[i][j]);
			printf("\n");
		}
	}
	Matrix operator *(Matrix k){
		Matrix res;
		//memset(res.a,0,sizeof(res.a));
		for(register int i=1; i<=2; i++)
			for(register int j=1; j<=2; j++)
				for(register int l=1; l<=2; l++)
					res.a[i][j] = (res.a[i][j]+a[i][l]*k.a[l][j]%MOD)%MOD;
		return res;
	}
}E,base,f;
ll n;

inline Matrix QuickPow(Matrix Base, ll k){
	Matrix res=E;
	while(k){
		if(k&1)
			res = res*Base;
		Base = Base*Base;
		k>>=1;
	}
	return res;
}

int main(){
	E.a[1][1]=E.a[1][2]=1;
	base.a[1][1]=base.a[1][2]=base.a[2][1]=1;
	f.a[1][1]=f.a[1][2]=1;
	Read(n);
	if(n<=2){
		cout<<1<<endl;
		return 0;
	}
	cout<<(f*QuickPow(base,n-2)).a[1][1]<<endl;
	return 0;
}

2.2 Luogu P1939 【模板】矩阵加速(数列)

题目链接
思路类似,但细节上稍微有些不一样

一次构造

考虑构造初始序列 A A A
题目求 f n f_n fn,注意到 f n = f n − 3 + f n − 1 f_n=f_{n-3}+f_{n-1} fn=fn3+fn1
所以 A = [ f n − 1 f n − 3 ⋯   ] A=[f_{n-1}\quad f_{n-3}\quad \cdots] A=[fn1fn3]
A ′ = [ f n f n − 2 ⋯   ] A'=[f_{n}\quad f_{n-2}\quad \cdots] A=[fnfn2]

二次构造

然后问题来了, f n f_n fn的确可以推出来,但是 f n − 2 f_{n-2} fn2却不能用 f n − 1 f_{n-1} fn1 f n − 3 f_{n-3} fn3推出来
所以我们还要再保存几个能推出 f n − 2 f_{n-2} fn2的值,这里直接保存 f n − 2 f_{n-2} fn2就好了,于是
A = [ f n − 1 f n − 2 f n − 3 ⋯   ] A=[f_{n-1}\quad f_{n-2}\quad f_{n-3}\quad \cdots] A=[fn1fn2fn3]
A ′ = [ f n f n − 1 f n − 2 ⋯   ] A'=[f_{n}\quad f_{n-1}\quad f_{n-2}\quad \cdots] A=[fnfn1fn2]
检查一下,对于 A ′ A' A的每一项:
f n = f n − 1 + f n − 3 f_n=f_{n-1}+f_{n-3} fn=fn1+fn3
f n − 1 = f n − 1 f_{n-1}=f_{n-1} fn1=fn1
f n − 2 = f n − 2 f_{n-2}=f_{n-2} fn2=fn2
所以 A A A便构造出来啦
同时初始序列 A = [ 1 1 1 ] A=[1\quad1\quad1] A=[111]

构造操作

再构造操作矩阵 B B B
观察前文从 A A A递推到 A ′ A' A的方式,便快速得出操作序列分别为:
[ 1 0 1 ] [1\quad 0\quad 1] [101]
[ 1 0 0 ] [1\quad 0\quad 0] [100]
[ 0 1 0 ] [0\quad 1\quad 0] [010]
再拼在一起(注意这里我们构造的是,拼的时候要转一下)便得到了操作矩阵 B = [ 1 1 0 0 0 1 1 0 0 ] B=\begin{bmatrix}1&1&0\\0&0&1\\1&0&0\end{bmatrix} B=101100010
A n s = A ⋅ B n Ans=A\cdot B^{n} Ans=ABn
代码就不给了,自己写一写要不然博客太长了

2.3 Vijos 1067 Warcraft III 守望者的烦恼

题目链接

一次构造

构造初始序列 A A A
考虑所求值 f n f_n fn,第 n n n个位置可以由第 n − 1 , n − 2 , n − 3 , ⋯   , n − k n-1,n-2,n-3,\cdots,n-k n1,n2,n3,,nk个位置闪过来,所以递推方程:
f n = ∑ i = n − k n − 1 f i f_n=\sum_{i=n-k}^{n-1}f_i fn=i=nkn1fi
所以直接把每一个 f i f_i fi都存进来
A = [ f n − 1 f n − 2 ⋯ f n − k ⋯   ] A=[f_{n-1}\quad f_{n-2}\quad \cdots \quad f_{n-k} \quad \cdots] A=[fn1fn2fnk]
检验一下
A ′ = [ f n f n − 1 ⋯ f n − k + 1 ⋯   ] A'=[f_n\quad f_{n-1}\quad \cdots \quad f_{n-k+1}\quad \cdots] A=[fnfn1fnk+1]
其中
f n = f n − 1 + f n − 2 + ⋯ + f n − k f_n=f_{n-1}+f_{n-2}+\cdots+f_{n-k} fn=fn1+fn2++fnk
f n − 1 = f n − 1 f_{n-1}=f_{n-1} fn1=fn1
⋯ \cdots
f n − k + 1 = f n − k + 1 f_{n-k+1}=f_{n-k+1} fnk+1=fnk+1
所以可以从 A A A递推出 A ′ A' A

构造操作

对于 f n f_n fn,操作序列为 [ 1 1 ⋯ 1 ] ( k 个 ) [1\quad1\quad\cdots\quad1](k\text{个}) [111](k)
对于其他的项,操作序列分别为:
[ 1 0 0 0 ⋯ 0 ] [1\quad0\quad0\quad0\quad\cdots\quad0] [10000]
[ 0 1 0 0 ⋯ 0 ] [0\quad1\quad0\quad0\quad\cdots\quad0] [01000]
[ 0 0 1 0 ⋯ 0 ] [0\quad0\quad1\quad0\quad\cdots\quad0] [00100]
⋯ \cdots
[ 0 0 0 ⋯ 1 0 ] [0\quad0\quad0\quad\cdots\quad1\quad0] [00010]
∴ B = [ 1 1 0 ⋯ 0 1 0 1 ⋯ 0 ⋯ 1 0 0 ⋯ 1 1 0 0 ⋯ 0 ] \therefore B=\begin{bmatrix}1&1&0&\cdots&0\\1&0&1&\cdots&0\\&&\cdots\\1&0&0&\cdots&1\\1&0&0&\cdots&0\end{bmatrix} B=1111100001000010

2.4 等比数列求和

未知来源
众所周知等比数列有求和公式可以直接用,然而某毒瘤出题人xinyue就是要让你对合数取模,所以我们就考虑不用除法的方法——矩阵快速幂!(当然,大佬们可以随意用数论处理合数模数的方法暴踩我)

一次构造

显然 S n = S n − 1 + p n S_n=S_{n-1}+p^n Sn=Sn1+pn
所以构造 A = [ S n − 1 p n − 1 ] A=[S_{n-1}\quad p^{n-1}] A=[Sn1pn1]
考虑递推 A ′ = [ S n p n ] A'=[S_n\quad p^n] A=[Snpn]
递推过程显然
S n = S n − 1 + p n − 1 S_n=S_{n-1}+p^{n-1} Sn=Sn1+pn1
p n = p n − 1 ⋅ p p^n=p^{n-1}\cdot p pn=pn1p
A A A便构造好啦

构造操作

再构造操作矩阵,同样观察系数构造出操作序列:
[ 1 1 ] [1\quad 1] [11]
[ 0 p ] [0\quad p] [0p]
于是操作矩阵 B = [ 1 0 1 p ] B=\begin{bmatrix}1&0\\1&p\end{bmatrix} B=[110p]

3 小结

3.1 难点

很多人都以为矩阵快速幂难点在于递推式,其实我觉得它真正的重难点在于构造初始序列 A A A,但这其实也是一个很简单的过程。这个过程和 B F S BFS BFS十分地相似,先将所求的 f n f_n fn存入“队列”,每次取出“队头”,将推出“队头”需要的东西再次存入“队列”,直到“队列”为空为止。

3.2 常见问题

矩阵快速幂的操作矩阵 B B B可以有很多种不同的样子,毕竟初始序列 A A A可以有很多不同的写法,所以当你发现你和小伙伴的写法不一样的时候,不要去盲目修改自己的写法。毕竟一万个矩阵快速幂的题解有一万种不同的操作矩阵

3.3 建议

把矩阵用结构体存起来,同时重载乘号,这样写快速幂的时候可以直接套用快速幂的模板,而且代码看起来也很简洁

4 致谢

感谢wbk大佬用一晚上的时间和我一起研究矩阵
感谢xinyue的题库和讲解

你可能感兴趣的:(矩阵快速幂,算法)