快速幂矩乘学习报告

科学到了一定境界就是玄学,玄学到了一定境界就是科学。
——题记

快速幂矩乘学习报告

矩阵是一个非常庞大和复杂的课题,本文只涉及最简单的矩阵运算和优化。
对于更多内容,详见OI Wiki 数学/线性代数/矩阵一节。

矩阵及其运算的基本性质

矩阵加法/矩阵减法:对应位置相加减
矩阵加法部分代码如下:

const matrix operator + (const matrix &x,const matrix &y){
	matrix ret;
	int i,j;
	ret.l = x.l,ret.r = x.r;
	for(i = 1;i <= ret.l;i++){
		for(j = 1;j <= ret.r;j++){
			ret.mat[i][j] = 0;
		}
	}
	for(i = 1;i <= ret.l;i++){
		for(j = 1;j <= ret.r;j++){
			ret.mat[i][j] = (x.mat[i][j] + y.mat[i][j]) % mod;
		}
	}
	return ret;
}

矩阵乘法:结果矩阵中的点 ( i , j ) (i,j) (i,j)为左矩阵 i i i行与右矩阵 j j j列对应位置的积的和。
矩阵乘法要求左矩阵的列数等于右矩阵的行数,这样才能一一对应。
矩阵乘法满足分配律和结合律,但是不满足交换律。

在矩阵乘法当中, a n s [ i ] [ j ] = x [ i ] [ k ] ∗ y [ k ] [ j ] ans[i][j]=x[i][k]*y[k][j] ans[i][j]=x[i][k]y[k][j],先枚举k,再枚举i,j能优化常数,把j用register int声明能进一步优化常数。

矩阵乘法部分代码如下:

const matrix operator * (const matrix &x,const matrix &y){
	matrix ret;
	int i,k;
	register int j;
	long long temp;
	ret.l = x.l,ret.r = y.r;
	for(i = 1;i <= ret.l;i++){
		for(j = 1;j <= ret.r;j++){
			ret.mat[i][j] = 0;
		}
	}
	for(k = 1;k <= x.r;k++){
		for(i = 1;i <= ret.l;i++){
			temp = x.mat[i][k];
			for(j = 1;j <= ret.r;j++){
				ret.mat[i][j] = (ret.mat[i][j] + temp * y.mat[k][j]) % mod; 
			}
		}
	}
	return ret;
}

矩阵快速幂:优化求矩阵的幂的方法。

单位矩阵:只有对角线是1,剩余部分都是0的矩阵。一切矩阵乘单位矩阵仍等于它本身。

矩阵运算的应用

矩阵运算一般用来优化一些线性的、符合某些递推关系的计算过程的时间复杂度(但矩阵快速幂的常数是绝对不能忽视的),也能简化多个元素共同计算的问题。矩阵运算的优化一般都是用矩阵乘法。

矩阵运算优化时间复杂度

矩阵运算能够优化的递推关系中常见的如下:

  • f [ i ] = p f [ i − x ] + q f [ i − y ] ⋯ f[i]=pf[i-x]+qf[i-y]\cdots f[i]=pf[ix]+qf[iy]
  • g [ i ] [ j ] = g [ i ] [ k ] + g [ k ] [ j ] g[i][j]=g[i][k]+g[k][j] g[i][j]=g[i][k]+g[k][j]

总而言之,通常可以优化的递推式接近于矩阵乘法的形式。

T1 广义斐波那契数列(洛谷P1349,难度2)
这题的递推式就是一个线性的普通递推,因此考虑用矩阵加速递推。为了得到转移矩阵,由于这里 f [ i ] f[i] f[i]只与 f [ i − 1 ] f[i-1] f[i1]有关,因此我们理论上只需要考虑2*2的矩阵。
先列出这么一个形式的递推:
[ f [ i ] f [ i − 1 ] ] = [ f [ i − 1 ] f [ i − 2 ] ] ∗ [ ? ? ? ? ] \begin{bmatrix}f[i]&f[i-1]\end{bmatrix}=\begin{bmatrix}f[i-1]&f[i-2]\end{bmatrix}*\begin{bmatrix}?& ?\\?& ?\end{bmatrix} [f[i]f[i1]]=[f[i1]f[i2]][????]

我们接下来考虑矩阵乘法的定义,套入原来的式子 f [ i ] = p f [ i − 1 ] + q f [ i − 2 ] f[i]=pf[i-1]+qf[i-2] f[i]=pf[i1]+qf[i2],就可以得到矩阵递推式:
[ f [ i ] f [ i − 1 ] ] = [ f [ i − 1 ] f [ i − 2 ] ] ∗ [ p 1 q 0 ] \begin{bmatrix}f[i]&f[i-1]\end{bmatrix}=\begin{bmatrix}f[i-1]&f[i-2]\end{bmatrix}*\begin{bmatrix}p&1\\q& 0\end{bmatrix} [f[i]f[i1]]=[f[i1]f[i2]][pq10]

最终答案如下:

[ f [ i ] f [ i − 1 ] ] = [ a 2 a 1 ] ∗ [ p 1 q 0 ] n − 2 \begin{bmatrix}f[i]&f[i-1]\end{bmatrix}=\begin{bmatrix}a_2&a_1\end{bmatrix}*\begin{bmatrix}p&1\\q& 0\end{bmatrix}^{n-2} [f[i]f[i1]]=[a2a1][pq10]n2

因此,对于这类问题,一个比较可行的办法就是先列出结果和转移出结果的两个矩阵,然后通过这两个矩阵填写转移矩阵。

T2 行为方案(难度3)

  • 某个国家用一个机器人攻击一个有n个城市、m个道路的地区,机器人每一次行动可以进行以下三项之一:原地不动,运动到可达的相邻城市,自爆。机器人从1号城市出发,求机器人进行t次行动的全部方案数。

d p [ i ] [ j ] dp[i][j] dp[i][j]表示机器人从i走到j的总方案数,枚举中转城市,则有 d p [ i ] [ j ] = ∑ d p [ i ] [ k ] + d p [ k ] [ j ] dp[i][j]=\sum dp[i][k] +dp[k][j] dp[i][j]=dp[i][k]+dp[k][j],这个形式明显是矩阵乘法的形式,可以考虑用矩阵优化。把 d p [ i ] [ j ] dp[i][j] dp[i][j]就看作是矩阵 ( i , j ) (i,j) (i,j)的值,那么两个城市联通可以表示为 d p [ x ] [ y ] = d p [ y ] [ x ] = 1 dp[x][y] = dp[y][x] = 1 dp[x][y]=dp[y][x]=1,原地不动相当于 d p [ x ] [ x ] = 1 dp[x][x]=1 dp[x][x]=1,自爆看作走到城市0,即 d p [ x ] [ 0 ] = 1 dp[x][0]=1 dp[x][0]=1.按照这些规则构造矩阵,然后计算它的t次方,取出x=1的值加和就是最终答案。
代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int mod = 2017;
struct matrix{
	long long l,r,mat[101][101];
}A,tot;
int n;
matrix GetE(){
	matrix ret;
	int i;
	ret.l = ret.r = n;
	for(i = 0;i <= n;i++){
		ret.mat[i][i] = 1;
	}
	return ret;
}
matrix operator * (const matrix &x,const matrix &y){
	matrix ret;
	int i,j,k;
	long long temp;
	ret.l = x.l,ret.r = y.r;
	for(i = 0;i <= ret.l;i++){
		for(j = 0;j <= ret.r;j++){
			ret.mat[i][j] = 0;//必要的初始化
		}
	}
	for(k = 0;k <= x.r;k++){
		for(i = 0;i <= ret.l;i++){
			temp = x.mat[i][k];
			for(j = 0;j <= ret.r;j++){
				ret.mat[i][j] = (ret.mat[i][j] + temp * y.mat[k][j] % mod) % mod;
			}
		}
	}
	return ret;
}
matrix ksm(matrix a,int b){
	matrix ret = GetE();
	while(b){
		if(b & 1){
			ret = ret * a;
		}
		a = a * a;
		b >>= 1;
	}
	return ret;
}
int main(){
	int i,j,m,t,x,y;
	long long res = 0;
	scanf("%d %d",&n,&m);
	for(i = 0;i <= n;i++){
		A.mat[i][i] = 1;
		A.mat[i][0] = 1;
	}
	for(i = 1;i <= m;i++){
		scanf("%d %d",&x,&y);
		A.mat[x][y] = A.mat[y][x] = 1;
	}
	scanf("%d",&t);
	A.l = A.r = n;
	tot = ksm(A,t);
	for(i = 0;i <= n;i++){
		res = (res + tot.mat[1][i]) % mod;
	}
	printf("%lld\n",res);
	return 0;
}
/*
3 2
1 2
2 3
2
*/

矩阵运算优化实现过程

T3 大魔法师(洛谷P7453)
此题首先全是区间操作,无疑要上线段树了。
这题麻烦的地方在于怎么同时更新三个数组。一个比较简单直接的想法是用结构体存,但是这就需要同时写区间赋值、区间加、区间乘,尤其是怎么把区间赋值和后两个很好的匹配(毕竟肯定要打懒标记的)是一个比较头疼的问题。再加上同时区间加和区间乘,那一天OIer又回想起了被线段树2pushdown支配的恐惧
然而如果我们换个思路,用矩阵乘法来考虑这个问题,会有一些新的发现。以操作一和操作四为例:
[ A i B i C i ] ∗ [ 1 0 0 1 1 0 0 0 1 ] = [ A i + B i B i C i ] ( 1 ) \begin{bmatrix}A_i&B_i&C_i\end{bmatrix}*\begin{bmatrix}1& 0&0\\1& 1&0\\0&0&1\end{bmatrix}=\begin{bmatrix}A_i+B_i&B_i&C_i\end{bmatrix}(1) [AiBiCi]110010001=[Ai+BiBiCi](1)

[ A i B i C i 1 ] ∗ [ 1 0 0 0 0 1 0 0 0 0 1 0 v 0 0 1 ] = [ A i + v B i C i 1 ] ( 4 ) \begin{bmatrix}A_i&B_i&C_i&1\end{bmatrix}*\begin{bmatrix}1& 0&0&0\\0& 1&0&0\\0&0&1&0\\v&0&0&1\end{bmatrix}=\begin{bmatrix}A_i+v&B_i&C_i&1\end{bmatrix}(4) [AiBiCi1]100v010000100001=[Ai+vBiCi1](4)

(当然,为了统一,我们实际写的时候对于123操作的矩阵也要变成4x4和1x4的)
这样一来,对于全部的六个修改操作,就可以全部用矩阵乘法解决,维护区间三种元素的和只要再相加就可以了。这样一来,我们只需要在区间乘区间求和的基础上,套上矩阵就完成了此题。

此题的六个转移式在代码后我会完整地列出。

代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define mid (l + r >> 1)
const int mod = 998244353;
const int N = 250001;
struct matrix{
	int l,r;
	long long mat[5][5];
}I,emp,op[7];
int a[N],b[N],c[N];
const matrix operator * (const matrix &x,const matrix &y){
	matrix ret;
	int i,k;
	register int j;
	long long temp;
	ret.l = x.l,ret.r = y.r;
	for(i = 1;i <= ret.l;i++){
		for(j = 1;j <= ret.r;j++){
			ret.mat[i][j] = 0;
		}
	}
	for(k = 1;k <= x.r;k++){
		for(i = 1;i <= ret.l;i++){
			temp = x.mat[i][k];
			for(j = 1;j <= ret.r;j++){
				ret.mat[i][j] = (ret.mat[i][j] + temp * y.mat[k][j]) % mod; 
			}
		}
	}
	return ret;
}
const matrix operator + (const matrix &x,const matrix &y){
	matrix ret;
	int i,j;
	ret.l = x.l,ret.r = x.r;
	for(i = 1;i <= ret.l;i++){
		for(j = 1;j <= ret.r;j++){
			ret.mat[i][j] = 0;
		}
	}
	for(i = 1;i <= ret.l;i++){
		for(j = 1;j <= ret.r;j++){
			ret.mat[i][j] = (x.mat[i][j] + y.mat[i][j]) % mod;
		}
	}
	return ret;
}
void GetI(){
	int i,j;
	I.l = I.r = 4;
	for(i = 1;i <= 4;i++){
		for(j = 1;j <= 4;j++){
			I.mat[i][j] = (i == j);
		}
	}
	emp.l = 1,emp.r = 4;
	for(i = 1;i <= 4;i++){
		emp.mat[1][i] = 0;
	}
	for(i = 1;i <= 6;i++) op[i].l = op[i].r = 4;
	op[1].mat[1][1] = op[1].mat[2][1] = op[1].mat[2][2] = op[1].mat[3][3] = op[1].mat[4][4] = 1;
	op[2].mat[1][1] = op[2].mat[2][2] = op[2].mat[3][2] = op[2].mat[3][3] = op[2].mat[4][4] = 1;
	op[3].mat[1][1] = op[3].mat[2][2] = op[3].mat[1][3] = op[3].mat[3][3] = op[3].mat[4][4] = 1;
	op[4].mat[1][1] = op[4].mat[2][2] = op[4].mat[3][3] = op[4].mat[4][4] = 1;
	op[5].mat[1][1] = op[5].mat[3][3] = op[5].mat[4][4] = 1;
	op[6].mat[1][1] = op[6].mat[2][2] = op[6].mat[4][4] = 1;
}
struct wyx{
	matrix tre[N << 2],laz[N << 2];
	void push(int k,int l,int r){
		laz[k << 1] = laz[k << 1] * laz[k];
		laz[k << 1 | 1] = laz[k << 1 | 1] * laz[k];
		tre[k << 1] = tre[k << 1] * laz[k];
		tre[k << 1 | 1] = tre[k << 1 | 1] * laz[k];
		laz[k] = I;
	}
	void build(int k,int l,int r){
		laz[k] = I;
		tre[k].l = 1,tre[k].r = 4;
		if(l == r){
			tre[k].mat[1][1] = a[l],tre[k].mat[1][2] = b[l],tre[k].mat[1][3] = c[l],tre[k].mat[1][4] = 1;
			return;
		}
		build(k << 1,l,mid);
		build(k << 1 | 1,mid + 1,r);
		tre[k] = tre[k << 1] + tre[k << 1 | 1];
	}
	void modify(int k,int l,int r,int x,int y,int id){
		if(x <= l && r <= y){
			laz[k] = laz[k] * op[id];
			tre[k] = tre[k] * op[id];
			return;
		}
		push(k,l,r);
		if(x <= mid) modify(k << 1,l,mid,x,y,id);
		if(y > mid) modify(k << 1 | 1,mid + 1,r,x,y,id);
		tre[k] = tre[k << 1] + tre[k << 1 | 1];
	}
	matrix query(int k,int l,int r,int x,int y){
		matrix ret = emp;
		if(x <= l && r <= y){
			return tre[k];
		}
		push(k,l,r);
		if(x <= mid) ret = ret + query(k << 1,l,mid,x,y);
		if(y > mid) ret = ret + query(k << 1 | 1,mid + 1,r,x,y);
		return ret;
	}
}STr;
int main(){
	int i,n,m,x,y,v,z;
	matrix temp;
	scanf("%d",&n);
	for(i = 1;i <= n;i++){
		scanf("%d %d %d",&a[i],&b[i],&c[i]);
	}
	GetI();
	STr.build(1,1,n);
	scanf("%d",&m);
	for(i = 1;i <= m;i++){
		scanf("%d %d %d",&z,&x,&y);
		if(z <= 6){
			if(z >= 4 && z <= 6){
				scanf("%d",&v);
				op[4].mat[4][1] = op[5].mat[2][2] = op[6].mat[4][3] = v;
			}
			STr.modify(1,1,n,x,y,z);
			op[4].mat[4][1] = op[5].mat[2][2] = op[6].mat[4][3] = 0;
		}
		else{
			temp = STr.query(1,1,n,x,y);
			printf("%lld %lld %lld\n",temp.mat[1][1],temp.mat[1][2],temp.mat[1][3]);
		}
	}
	return 0;
}

在所有的矩阵运算之前,必须给矩阵的长宽赋值,这在复杂的问题当中尤其容易被忽略。

全部转移式如下:

[ A i B i C i 1 ] ∗ [ 1 0 0 0 1 1 0 0 0 0 1 0 0 0 0 1 ] = [ A i + B i B i C i 1 ] ( 1 ) \begin{bmatrix}A_i&B_i&C_i&1\end{bmatrix}*\begin{bmatrix}1& 0&0&0\\1& 1&0&0\\0&0&1&0\\0&0&0&1\end{bmatrix}=\begin{bmatrix}A_i+B_i&B_i&C_i&1\end{bmatrix}(1) [AiBiCi1]1100010000100001=[Ai+BiBiCi1](1)

[ A i B i C i 1 ] ∗ [ 1 0 0 0 0 1 0 0 0 1 1 0 0 0 0 1 ] = [ A i B i + C i C i 1 ] ( 2 ) \begin{bmatrix}A_i&B_i&C_i&1\end{bmatrix}*\begin{bmatrix}1& 0&0&0\\0& 1&0&0\\0&1&1&0\\0&0&0&1\end{bmatrix}=\begin{bmatrix}A_i&B_i+C_i&C_i&1\end{bmatrix}(2) [AiBiCi1]1000011000100001=[AiBi+CiCi1](2)

[ A i B i C i 1 ] ∗ [ 1 0 1 0 0 1 0 0 0 0 1 0 0 0 0 1 ] = [ A i B i C i + A i 1 ] ( 3 ) \begin{bmatrix}A_i&B_i&C_i&1\end{bmatrix}*\begin{bmatrix}1& 0&1&0\\0& 1&0&0\\0&0&1&0\\0&0&0&1\end{bmatrix}=\begin{bmatrix}A_i&B_i&C_i+A_i&1\end{bmatrix}(3) [AiBiCi1]1000010010100001=[AiBiCi+Ai1](3)

[ A i B i C i 1 ] ∗ [ 1 0 0 0 0 1 0 0 0 0 1 0 v 0 0 1 ] = [ A i + v B i C i 1 ] ( 4 ) \begin{bmatrix}A_i&B_i&C_i&1\end{bmatrix}*\begin{bmatrix}1& 0&0&0\\0& 1&0&0\\0&0&1&0\\v&0&0&1\end{bmatrix}=\begin{bmatrix}A_i+v&B_i&C_i&1\end{bmatrix}(4) [AiBiCi1]100v010000100001=[Ai+vBiCi1](4)

[ A i B i C i 1 ] ∗ [ 1 0 0 0 0 v 0 0 0 0 1 0 0 0 0 1 ] = [ A i + v B i ∗ v C i 1 ] ( 5 ) \begin{bmatrix}A_i&B_i&C_i&1\end{bmatrix}*\begin{bmatrix}1& 0&0&0\\0& v&0&0\\0&0&1&0\\0&0&0&1\end{bmatrix}=\begin{bmatrix}A_i+v&B_i*v&C_i&1\end{bmatrix}(5) [AiBiCi1]10000v0000100001=[Ai+vBivCi1](5)

[ A i B i C i 1 ] ∗ [ 1 0 0 0 0 1 0 0 0 0 0 0 0 0 v 1 ] = [ A i B i v 1 ] ( 6 ) \begin{bmatrix}A_i&B_i&C_i&1\end{bmatrix}*\begin{bmatrix}1& 0&0&0\\0& 1&0&0\\0&0&0&0\\0&0&v&1\end{bmatrix}=\begin{bmatrix}A_i&B_i&v&1\end{bmatrix}(6) [AiBiCi1]10000100000v0001=[AiBiv1](6)

总而言之,矩阵是一个复杂的,也是一个相对比较陌生的元素,可能用起来比较玄学,比较难以理解,但是如果能够分清何时使用矩阵优化、能够比较清晰地在矩阵上推导,那么矩阵运算会成为一种非常好的优化手段。
Thank you for reading!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值