科学到了一定境界就是玄学,玄学到了一定境界就是科学。
——题记
快速幂矩乘学习报告
矩阵是一个非常庞大和复杂的课题,本文只涉及最简单的矩阵运算和优化。
对于更多内容,详见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[i−x]+qf[i−y]⋯
- 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[i−1]有关,因此我们理论上只需要考虑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[i−1]]=[f[i−1]f[i−2]]∗[????]
我们接下来考虑矩阵乘法的定义,套入原来的式子
f
[
i
]
=
p
f
[
i
−
1
]
+
q
f
[
i
−
2
]
f[i]=pf[i-1]+qf[i-2]
f[i]=pf[i−1]+qf[i−2],就可以得到矩阵递推式:
[
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[i−1]]=[f[i−1]f[i−2]]∗[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[i−1]]=[a2a1]∗[pq10]n−2
因此,对于这类问题,一个比较可行的办法就是先列出结果和转移出结果的两个矩阵,然后通过这两个矩阵填写转移矩阵。
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+vBi∗vCi1](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!