算法简介:正绢六通

首先,说到矩阵乘法。字面意思的来讲,就是矩阵与矩阵之间的乘法,他们之间是否满足正常数字之间的乘法规则,也就是交换律与结合律呢?这部分的内容都将在下面被解释。

算法演示:落染五色

矩阵加法:缀锦四分

这个比较简单。首先我们了解一下矩阵的定义,对于一个 $n$ 行 $m$ 列的矩阵,我们有如下的表示方式:

这样的矩阵,就是我们接下来的基础。首先讲到乘法,肯定会有人想到加法。但是实际上……矩阵的加法和乘法几乎一点关系都没有……但是提到了我们也顺便讲一下。

矩阵的运算不同于数字,两个矩阵之间是否可以进行运算,对于矩阵的行和列有着严格的规定。比如说加法,所对应的两个矩阵行和列必须一模一样,因为矩阵加法,就是两个长宽一样的矩阵内的元素一一相加。且满足交换律和结合律。这里给出矩阵的基本定义,以及加法的简单代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Matrix {
public:
int n, m;
vector<vector<ll > > mat;

Matrix(int n, int m) : n(n), m(m), mat(n, vector<ll >(m, 0)) {}

Matrix operator+(const Matrix& B) const {
if(n != B.n || m != B.m) {
return *this;
}
Matrix result(n, m);
for(int i = 0; i < n; ++i) {
for(int j = 0; j < m; ++j) {
result.mat[i][j] = (mat[i][j] + B.mat[i][j]) % P;
}
}
return result;
}
};

矩阵乘法:衣裁三礼

矩阵之间的乘法,运算的规则比较繁杂。我们说矩阵乘法,需要用第一个乘数矩阵的每一行,去乘以第二个乘数矩阵的每一列。具体的运算过程,可以用下面的表示法表示:

不难发现,我们上面所说的每一行去乘以每一列,其实就说明了这个矩阵乘法的要求,第一个乘数矩阵的列数,要和第二个乘数的行数相等,不然的话做乘法的时候个数对不上。我们用如下的方式表示连个乘法相乘后的结果,也就是当矩阵 $a$ 乘以矩阵 $b$ 的时候,答案矩阵 $c$ 如何表示。

其中我们规定,$a$ 是一个 $r \times c$ 的矩阵,$b$ 是一个 $c\times p$ 的矩阵,那么不难发现答案数组 $c$ 应该是 $r\times p$ 的矩阵。我们可以再举一个例子:

连着几个例子看下来,估计已经可以找到矩阵乘法的规律,并且,不难观察出,这样的乘法形式显而易见不支持交换律,因为他的要求必须精确到某一个乘数的行数或者列数。接下来要提到一个重要的表示法:我们其实是可以用矩阵乘法的表示形式,来描述一个多元一次方程的。比如说下面给出一个二元一次方程组以及他的矩阵表示形式:

矩阵表示,就应该是夏眠这样的,我们称第一个矩阵为系数矩阵,最后一个矩阵为结果矩阵。

这样的表示法极为重要,是接下来构造递推矩阵的答案的非常重要的方式。这里附上矩阵乘法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Matrix operator*(const Matrix& B) const {
if(m != B.n) {
return *this;
}
Matrix result(n, B.m);
for(int i = 0; i < n; ++i) {
for(int j = 0; j < B.m; ++j) {
result.mat[i][j] = 0;
for(int k = 0; k < m; ++k) {
result.mat[i][j] = (result.mat[i][j] + mat[i][k] * B.mat[k][j] % P) % P;
}
}
}
return result;
}

矩阵的幂:绫羽二重

说到乘法和幂,不难直接想到我们曾经学过的利器:快速幂。那么矩阵,是否可以用快速幂来计算呢?的确可以,我们回顾一下快速幂的过程。

不难看出来,快速幂的底层逻辑,其实本质上是运用了乘法结合律,而矩阵乘法也可以满足这样的规律。那么也就是说,矩阵也可以有自己的矩阵快速幂。

那么其实有了矩阵乘法的封装,矩阵快速幂的代码就变得极其好写,这一小目的主要内容其实重在矩阵快速幂的运用。下面看一道例题。

求普通斐波那契数列的第 $n$ 项对于 $10^9+7$ 取模的结果( $1\le n\le 2^{31}-1$ )

那么这个数据范围其实已经很敏感了。这样 $n$ 的大小甚至支持不了 $O(n)$ 的复杂度。那么我们想想怎么推出斐波那契数列的递推矩阵。我们首先写出斐波那契数列每一位数的关系:

那么我们的目标,是用矩阵递推出后面的内容。可以尝试上面的列方程的形式进行转化。

那么转化的矩阵就是:

转化的过程中一定要注意,他们是关于谁的方程,上下两个式子都特意用了 $f_1,f_2$ 来表示,就是为了凑一个整,可以用方程的形式表达出来,答案那边的 $f_2,f_3$ 只是作为结果出现而已。

并且,推递推矩阵的时候一定要注意:

  1. 必须得是 $n\times n$ 的矩阵,不然不能快速幂
  2. 每一个系数都得按照设定未知数的顺序来,如果没有这一项,系数就是 0

这样就可以过掉这道经典的矩阵快速幂题,构造矩阵的方法主要就是这样用方程展示出来的做法,下面放上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include<bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef __int128 int128;
typedef pair<int , int > pii;
typedef unsigned long long ull;

namespace FastIO
{
// char buf[1 << 20], *p1, *p2;
// #define getchar() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 20, stdin), p1 == p2) ? EOF : *p1++)
template<typename T> inline T read(T& x) {
x = 0;
int f = 1;
char ch;
while (!isdigit(ch = getchar())) if (ch == '-') f = -1;
while (isdigit(ch)) x = (x << 1) + (x << 3) + (ch ^ 48), ch = getchar();
x *= f;
return x;
}
template<typename T, typename... Args> inline void read(T& x, Args &...x_) {
read(x);
read(x_...);
return;
}
inline ll read() {
ll x;
read(x);
return x;
}
};
using namespace FastIO;

const int N = 10;

int n, P;

struct Matrix {
int r, c;
ll a[N + 1][N + 1];

Matrix(){}
Matrix(const int &_r, const int &_c) : r(_r), c(_c) {
memset(a, 0, sizeof(a));
}

inline void clear() {
memset(a, 0, sizeof(a));
for (int i = 1; i <= r; i++) a[i][i] = 1;
}
inline Matrix operator * (const Matrix &rhs) const {
Matrix res(r, rhs.c);
for (int i = 1; i <= r; i++)
for (int j = 1; j <= rhs.c; j++) {
for (int k = 1; k <= c; k++) {
res.a[i][j] += a[i][k] * rhs.a[k][j] % P;
res.a[i][j] %= P;
}
}
return res;
}
inline Matrix operator ^ (int p) const {
Matrix res(r, c), x = *this;
res.clear();
for (; p; p >>= 1, x = x * x)
if (p & 1) res = res * x;
return res;
}
};

Matrix ans(2, 2);

inline void Input() {
read(n, P);
}

inline void Work() {
ans.a[1][1] = ans.a[1][2] = ans.a[2][1] = 1;
printf("%lld\n", (ans^(n-1)).a[1][1]);
}

int main() {
int T = 1;
while(T--) {
Input();
Work();
}
return 0;
}

算法实战:万理一空

查看相关算法标签喵!