(简记)矩阵快速幂 矩阵加速递推 动态 DP

矩阵乘法和矩阵幂

一个 \(m \times n\)矩阵是一个由 \(m\)\(n\) 列元素排列成的矩形阵列。即形如

\[A = \begin{bmatrix} a_{1 1} & a_{1 2} & \cdots & a_{1 n} \\ a_{2 1} & a_{2 2} & \cdots & a_{2 n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{m 1} & a_{m 2} & \cdots & a_{m n} \end{bmatrix} \text{.} \]

本题中认为矩阵中的元素 \(a_{i j}\) 是整数。

两个大小分别为 \(m \times n\)\(n \times p\) 的矩阵 \(A, B\) 相乘的结果为一个大小为 \(m \times p\) 的矩阵。将结果矩阵记作 \(C\),则

\[c_{i,j} = \sum_{k = 1}^{n} a_{i,k} b_{k,j} \text{,\qquad($1 \le i \le m$, $1 \le j \le p$).} \]

而如果 \(A\) 的列数与 \(B\) 的行数不相等,则无法进行乘法。

可以验证,矩阵乘法满足结合律,即 \((A B) C = A (B C)\)

一个大小为 \(n \times n\) 的矩阵 \(A\) 可以与自身进行乘法,得到的仍是大小为 \(n \times n\) 的矩阵,记作 \(A^2 = A \times A\)。进一步地,还可以递归地定义任意高次方 \(A^k = A \times A^{k - 1}\),或称 \(A^k = \underbrace{A \times A \times \cdots \times A}_{k \text{ 次}}\)

特殊地,定义 \(A^0\) 为单位矩阵 \(I = \begin{bmatrix} 1 & 0 & \cdots & 0 \\ 0 & 1 & \cdots & 0 \\ \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & \cdots & 1 \end{bmatrix}\)

为什么要用到矩阵快速幂

当需要利用矩阵解决一些问题时,矩阵快速幂显得尤其好用。譬如当你知道一个序列的每个数与前若干个数的关系时,求出数列第\(k\)个数值(\(k\le 10^{18}\))对某个数取模时。

可以帮助你在\(O(n^3\log k)\)的时间内轻松解决问题。

具体实现(代码)

只要掌握一些operator与结构体相关知识,实现并不困难,具体过程如下。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=105,MOD=1e9+7;
int n,k;
struct matrix{
	int row,col;
	long long data[N][N];
	matrix(int r,int c,bool isI){
		row=r;
		col=c;
		memset(data,0,sizeof(data));
		if(isI)
			for(int i=0;i<row;i++)
				data[i][i]=1;
	}
};
matrix operator * (const matrix &a,const matrix &b){
	matrix c(a.row,b.col,false);
	for(int i=0;i<a.row;i++)
		for(int j=0;j<b.col;j++)
			for(int k=0;k<a.col;k++)
				c.data[i][j]=(c.data[i][j]+a.data[i][k]*b.data[k][j])%MOD;
	return c;
}
matrix powMatrix(matrix a,int k){
	matrix ans(a.row,a.col,true);
	for(;k;k>>=1){
		if(k&1)ans=ans*a;
		a=a*a;
	}
	return ans;
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>k;
	matrix fa(n,n,false);
	for(int i=0;i<n;i++){
		for(int j=0;j<n;j++){
			cin>>fa.data[i][j];
		}
	}
	fa=powMatrix(fa,k);
	for(int i=0;i<n;i++){
		for(int j=0;j<n;j++){
			cout<<fa.data[i][j]<<' ';
		}
		cout<<'\n';
	}
	return 0;
}

构造矩阵

重定义 动态 DP

矩阵乘法的普适定义是这样的:\(C_{i,j}=\sum A_{i,k}\times B_{k,j}\),其中应该满足 \(A\) 的列数与 \(B\) 的行数一致。

在此基础上,我们可以改变矩阵乘法原有定义,如把 \(\times\) 改成 \(+\),使其仍然具有结合律等普遍性质。这种改变方法在P4719 【模板】动态 DP这类问题中尤其常见。在动态 DP 问题中,你需要在树上动态维护一个树形 DP,其中需要涉及到对轻重儿子的操作与区分。

对于本题矩阵方面的剖析,由于树链剖分的结果是区间左端点(链顶)的 DFS 序小,右端点(链尾)的 DFS 序大,所以需要改变以往的矩阵乘法顺序。

具体地,原矩阵长这样:

\[\begin {bmatrix} f_{j,0} & f_{j,1} \end{bmatrix} *\begin {bmatrix} g_{i,0} & g_{i,1} \\ g_{i,0} & -\infty \end{bmatrix}= \begin {bmatrix} f_{i,0} & f_{i,1} \end{bmatrix} \]

重新构造顺序后,矩阵应该长这样:

\[\begin {bmatrix} g_{i,0} & g_{i,0} \\ g_{i,1} & -\infty \end{bmatrix} *\begin {bmatrix} f_{j,0} \\ f_{j,1} \end{bmatrix} =\begin {bmatrix} f_{i,0} \\ f_{i,1} \end{bmatrix} \]

具体来说,原构造矩阵 \(base\)\(base_{i,k}\) 指从 \(i\) 转移到 \(k\) 的权值。新构造矩阵 \(base\) 中的 \(base_{i,k}\) 指的是从 \(k\) 转移到 \(i\) 的权值。

运用这个转化方式,我们回到题目。

本题要求维护树形 DP 的最大独立集信息,那么可以每次 update 记录从节点 \(u\) 到链顶 \(top[u]\) 的矩阵累积,然后由于链顶一定是父亲的轻儿子,所以需要更新 \(fa[top[u]]\) 的相应信息。

矩阵加速递推

P1939 矩阵加速(数列)

题目描述

已知一个数列 \(a\),它满足:

\[a_x= \begin{cases} 1 & x \in\{1,2,3\}\\ a_{x-1}+a_{x-3} & x \geq 4 \end{cases} \]

\(a\) 数列的第 \(n\) 项对 \(10^9+7\) 取余的值。

此处我们需要一个\(1\times 3\)的矩阵,形如:

\[\begin {bmatrix} a_{x-2} & a_{x-1} & a_x \end{bmatrix} \]

由题,初始矩阵即为:

\[\begin {bmatrix} 1 & 1 & 1 \end{bmatrix} \]

构造一个\(3\times 3\)的矩阵,形如:

\[\begin {bmatrix} 0 & 0 & 1 \\ 1 & 0 & 0 \\ 0 & 1 & 1 \end{bmatrix} \]

其中第一列表示对第一个数的转移,第二列表示对第二个数的转移,以此类推。

具体地,如\((1,3)\)\((3,3)\)表示进行乘法后的第\(3\)的位置受到原矩阵第\(1\)\(3\)位置的影响,即为两者的和。

同理,如\((3,2)\)表示第\(2\)个位置为原矩阵第\(3\)为的数值。

如此便可构造出相应的矩阵。

P6435 「EZEC-1」数列

无法做出 key observation,于是无法转化出来。

需要注意到的是第 \(i\) 层的公差是 \((a+b)^{i-1}\),然后就可以得到每行的 \(A_1,A_2\),且下一行的 \(A_1'\) 仅跟 \(A_1,A_2\) 有关,令 \(f_i\) 表示第 \(i+1\) 行的第一项,那么依此就可以列出递推式:

\[\begin{aligned} f_i&=af_{i-1}+b(f_{i-1}+(a+b)^{i-1})+c \\&=(a+b)f_{i-1}+b(a+b)^{n-1}+c \end{aligned} \]

矩阵加速即可。

#include<bits/stdc++.h>
using namespace std;
typedef __int128 LL;
LL n,a,b,c,MOD;
struct mat{int r,c;LL d[3][3];};
mat calc,f,base;
const mat operator *(const mat &a,const mat &b){
	calc.r=a.r,calc.c=b.c;
	for(int i=0;i<a.r;i++)
		for(int j=0;j<b.c;j++){
			calc.d[i][j]=0;
			for(int k=0;k<a.c;k++)
				calc.d[i][j]=(calc.d[i][j]+a.d[i][k]%MOD*(b.d[k][j]%MOD)%MOD)%MOD;
		}
	return calc;
}
mat qkpow(mat x,LL y){
	mat res;
	res.r=x.r;
	res.c=x.c;
	for(int i=0;i<x.r;i++)
		for(int j=0;j<x.c;j++){
			if(i==j)res.d[i][j]=1;
			else res.d[i][j]=0;
		}
	while(y){
		if(y&1)res=res*x;
		x=x*x;
		y>>=1;
	}
	return res;
}
int main(){
	long long N,A,B,C,mod;
	cin>>N>>A>>B>>C>>mod;
	n=N,a=A,b=B,c=C,MOD=mod;
	f.r=1,f.c=3;
	base.r=3,base.c=3;
	f.d[0][0]=1,f.d[0][1]=1,f.d[0][2]=1;
	base.d[0][0]=a+b,base.d[1][0]=b;
	base.d[1][1]=a+b,base.d[2][0]=c;
	base.d[2][2]=1;
	f=f*qkpow(base,n-1);
	cout<<(long long)f.d[0][0];
	return 0;
}

P10581 [蓝桥杯 2024 国 A] 重复的串

\(f_{i,e,len}\) 表示 \([1,i]\) 匹配 \(e\) 次匹配到 \(S\) 中第 \(len\) 个的方案数。加速递推 \(i\) 维,每次枚举下一个要取什么字母即可,如果取满了 \(len=|S|\),就转移到 \(f_{i+1,e+1,nxt_{len}}\),其中 \(nxt_i\) 数组就是 KMP 的 \(\texttt{Fail}\) 数组。无法存储两维,乘开存到一维即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=105;
const LL MOD=998244353;
struct mat{
	int r,c;
	LL a[N][N];
	mat operator *(const mat&b)const {
		mat res;res.r=r;res.c=b.c;
		for(int i=0;i<r;i++)
			for(int j=0;j<b.c;j++)res.a[i][j]=0;
		for(int i=0;i<r;i++)
			for(int j=0;j<b.c;j++)
				for(int k=0;k<c;k++)
					(res.a[i][j]+=a[i][k]*b.a[k][j]%MOD)%=MOD;
		return res;
	}
}now,per;
int n,m,nxt[N];
char s[N];
inline int pack(int x,int y){return x*m+y;}
mat qkpow(mat x,int y){
	mat res=x;
	for(int i=0;i<res.r;i++)
		for(int j=0;j<res.c;j++){
			if(i==j)res.a[i][j]=1;
			else res.a[i][j]=0;
		}
	while(y){
		if(y&1)res=res*x;
		x=x*x;
		y>>=1;
	}
	return res;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>(s+1)>>n;m=strlen(s+1);
	int pos=0;
	for(int i=2;i<=m;i++){
		while(pos&&s[pos+1]!=s[i])pos=nxt[pos];
		if(s[pos+1]==s[i])nxt[i]=++pos;
		else nxt[i]=0;
	}
	now.r=1;now.c=m*3;
	now.a[0][0]=1;
	per.r=m*3;per.c=m*3;
	for(int e=0;e<=2;e++){
		for(int i=0;i<m;i++){
			for(char ch='a';ch<='z';ch++){
				pos=i;
				while(pos&&s[pos+1]!=ch)pos=nxt[pos];
				pos+=(s[pos+1]==ch);
				if(pos==m){
					if(e==2)continue;
					per.a[pack(e,i)][pack(e+1,nxt[m])]++;
				}
				else per.a[pack(e,i)][pack(e,pos)]++;
			}
		}
	}
	now=now*qkpow(per,n);
	LL res=0;
	for(int i=0;i<m;i++)
		res=(res+now.a[0][pack(2,i)])%MOD;
	cout<<res;
	return 0;
}

一些trick

如上这类矩阵问题内,如果存在序列推导的常数项或者指数项,可以相应地加入内容。

具体内容有待补充。

posted @ 2025-04-24 14:45  TBSF_0207  阅读(49)  评论(0)    收藏  举报