浅谈矩阵及其基本运算
这篇文章主要用于讲解矩阵及其应用。
由于作者太菜,如有失误欢迎指出。
不一定更好的阅读体验
本文主要讲解以下几个内容:
0. 高斯消元
模板题见 P3389。
高斯消元法是一个用来求解线性方程组的算法。
可以解出形如
的
高斯消元主要是运用两种消元方法:加减消元法和代入消元法,和初中生解一次方程组一样。
高斯消元的方法是,使用主元法,先进行加减消元,然后我们可以先求得
举个例子,方程
先写进矩阵:
然后通过主元法,系数化为
加减消元:
使用相同的方法:
可得
但是怎么回代呢?
我们每次操作的时候留下一个当前的方程,矩阵会变成这样:
回代,解得
如果一个方程没有了系数不为 No Solution。
复杂度
参考代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 110;
const double eps = 1e-6;
int n; double a[N][N];
bool gauss() {
int r = 0;
for(int c = 0; c < n; c++) {
int t = r;
for(int i = r; i < n; i++)
if(fabs(a[i][c]) > fabs(a[t][c]))
t = i;
if(fabs(a[t][c]) < eps) continue;
for(int i = c; i < n + 1; i++) swap(a[t][i], a[r][i]);
for(int i = n; i >= c; i--) a[r][i] /= a[r][c];
for(int i = r + 1; i < n; i++)
if(fabs(a[i][c]) > eps)
for(int j = n; j >= c; j--)
a[i][j] -= a[r][j] * a[i][c];
r++;
}
if(r < n) return 1;
for(int i = n - 1; i >= 0; i--) for(int j = i + 1; j < n; j++) a[i][n] -= a[j][n] * a[i][j];
return 0;
}
int main() {
cin >> n;
for(int i = 0; i < n; i++)
for(int j = 0; j < n + 1; j++)
cin >> a[i][j];
bool t = gauss();
if(t == 0) for(int i = 0; i < n; i++) printf("%.2lf\n", a[i][n]);
else puts("No Solution");
return 0;
}
类似的还有 P2455。
高斯消元在后面的逆矩阵和矩阵行列式中有用到,虽然不属于矩阵范畴,但还是提一下。
1. 矩阵的定义
进入正题:
在了解矩阵的运算之前,我们需要了解矩阵是啥。
一个
矩阵其实就是一个二维数组。
把数按照方形排列起来,就是一个是矩阵了。
与二维数组不同的是,矩阵具有许多操作。
但是矩阵的存储方法就是用二维数组存储的。
一般为了方便,可能会定义成一个结构体的形式:
struct mtr {
int a[N][N];
};
2. 矩阵加法
两个
不妨设
如果
矩阵加法具有交换律和结合律。
mtr add(mtr a, mtr b) {
mtr ans;
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
ans.a[i][j] = a.a[i][j] + b.a[i][j];
}
}
return ans;
}
例题:B2104
太过于水,不给代码。
复杂度
3. 矩阵减法
矩阵减法是矩阵加法的逆运算。
不妨设
这个也很好理解,复杂度
mtr add(mtr a, mtr b) {
mtr ans;
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
ans.a[i][j] = a.a[i][j] - b.a[i][j];
}
}
return ans;
}
4. 矩阵乘法
两个大小分别为
当然,一般的矩阵乘法都用函数和结构体封装起来会更好。
mtr mul(mtr a, mtr b) {
mtr ans; for(int i = 1; i <= m; i++) for(int j = 1; j <= p; j++) ans.a[i][j] = 0;
for(int i = 1; i <= m; i++)
for(int k = 1; k <= n; k++)
for(int j = 1; j <= p; j++)
ans.a[i][j] += a.a[i][k] * b.a[k][j];
return ans;
}
而如果
矩阵乘法满足结合律,即
这里介绍一个 Strassen 矩阵乘法,复杂度
早在清朝的时候,我国的古书《群经评议·周官二》就提到:“凡邦之有疾病者,疕疡者造焉,则使医分而治之,是亦不自医也。”
“分而治之”的思想,在 OI 中是非常重要的。
可以考虑用分块进行优化,使用分块矩阵,没错,将一个矩阵分成四块计算。
递归最简单的情况是
共需要
由此产生分治降阶的递归算法。
依此算法,计算
那
模板题 P4783
定义:对于一个矩阵
求逆矩阵的思路:
求
对
此时原来单位矩阵转化成逆矩阵。
比如说,求
首先,在右边添加
对左边消元可得
回代,得
每项除以系数,右半边就是答案。
再将分数用费马小定理对
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int P = 1e9 + 7, N = 410;
int Pow(int a,int b) {
int ans = 1;
while(b) {
if(b & 1) ans = ans * a % P;
a = a * a % P; b >>= 1;
}
return ans;
}
int a[N][N * 2];
signed main() {
int n, m; scanf("%lld", &n); m = 2 * n;
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= n; j++) scanf("%lld", &a[i][j]);
a[i][i + n] = 1;
}
for(int i = 1; i <= n; i++) {
int pos = i;
for(int j = i + 1; j <= n; j++) if(abs(a[j][i]) > abs(a[pos][i])) pos = j;
if(i != pos) swap(a[i], a[pos]); if(!a[i][i]) puts("No Solution"), exit(0);
int inv = Pow(a[i][i], P - 2);
for(int j = 1; j <= n; j++)
if(i != j) {
int x = a[j][i] * inv % P;
for(int k = i; k <= m; k++)
a[j][k] = ((a[j][k] - a[i][k] * x) % P + P) % P;
}
for(int j = 1; j <= m; j++) a[i][j] = a[i][j] * inv % P;
}
for(int i = 1; i <= n; i++) {
for(int j = n + 1; j <= m; j++) printf("%lld ", a[i][j]); puts("");
}
return 0;
}
求出了矩阵的逆元,矩阵除法就非常简单了,只需要再做一次乘法就可以了。
复杂度
矩阵的逆元有以下几个性质:
-
-
2.$ 若相同大小的矩阵 $A,B$ 均可逆,则 $AB$ 可逆,且 $(AB)^{-1}=B^{-1}A^{-1} -
3.$ 若 $A$ 可逆,则 $A^{k}$ 也可逆($k$ 是正整数),且 $({A^k})^{-1}=(A^{-1})^k
所以我们就可以求
7. 广义矩阵乘法
假设有矩阵
若
在图论中,一种常见的模式是令
例题:P2886
考虑用计数类的 dp。令原图邻接矩阵为
但是复杂度
此时可以用广义矩阵乘法去做。
得
但是这里要注意的是需要将点离散化,否则
离散化之后
参考代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1010; int id[N];
struct mtr {int a[N][N];} G; int n, m, k, s, t;
mtr mul(mtr a, mtr b) {
mtr ans; memset(ans.a, 0x3f, sizeof ans.a);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
for(int k = 1; k <= n; k++)
ans.a[i][j] = min(ans.a[i][j], a.a[i][k] + b.a[k][j]);
return ans;
}
mtr Pow(mtr a, int b) {
mtr ans = a; b--;
while(b) {
if(b & 1) ans = mul(ans, a);
a = mul(a, a); b >>= 1;
}
return ans;
}
signed main() {
cin >> k >> m >> s >> t; memset(G.a, 0x3f, sizeof G.a);
while(m--) {
int a, b, c; cin >> c >> a >> b; int u, v;
if(!id[a]) id[a] = ++n; u = id[a];
if(!id[b]) id[b] = ++n; v = id[b];
G.a[u][v] = G.a[v][u] = c;
}
s = id[s], t = id[t];
mtr ans = Pow(G, k);
cout << ans.a[s][t] << endl;
return 0;
}
8. 矩阵行列式
模板题见 P7112
矩阵行列式的表示方法:一个
一个
矩阵行列式的定义:
把一个
一个
矩阵行列式的性质:
-
\det(A^T)=\det(A) -
若
A 为三角形矩阵,则\det(A)=A 的对角元素的乘积。 -
若
A 的元素全为0 或者有两行或两列相等或成比例时,\det(A)=0 。 -
将
A 的每个元素乘以相同的数k ,矩阵行列值不变。 -
果行列式对应矩阵
A 中有一行(列),是对应两个矩阵B,C 中分别的2 行(列)所有元素之和,那么有\det(A)=\det(B)+\det(C) 。
接下来,问题是如何求矩阵行列式的值。
还是消元。
我们考虑一个情况,当一个矩阵任意一个位置出现
考虑可以通过消元和前面的性质使得矩阵中出现更多的
显然瞎转化肯定是不行的,我们要让运算次数尽可能少,否则会 TLE。
我们知道求解线性方程组的算法高斯消元。其实高斯消元在做增广矩阵行(初等)变换为行最简形时的步骤和转化相当类似。
我们现在考虑将矩阵一行(列)消成只有最后一个元素非
这里引入代数余子式的概念:
在一个
删除在
对于一个元素
枚举第
然而
参考代码(1.92s 极限冲过):
#include<bits/stdc++.h>
using namespace std;
#define I inline
#define W while
#define gc getchar
#define pc putchar
namespace SlowIO {
I int read() {
int x = 0; char ch = gc();
W(ch < '0' || ch > '9') ch = gc();
W(ch >= '0' && ch <= '9') x = x * 10 + (ch ^ 48), ch = gc();
return x;
}
I void Read(int &x) {x = read();}
} using namespace SlowIO;
int p, n; const int N = 610; int *A[N], a[N][N];
int solve() {
int flag = 1;
for(int i = 1; i <= n; i++) {
for(int j = i + 1; j <= n; j++) {
while(A[j][i]) {
int t = A[i][i] / A[j][i];
for(int k = i; k <= n && t; k++)
A[i][k] = ((A[i][k] - 1ll * t * A[j][k] % p) % p + p) % p;
swap(A[i], A[j]);
flag = -flag;
}
}
}
int ret = flag;
for(int i = 1; i <= n; i++) ret = 1ll * ret * A[i][i] % p;
return (ret + p) % p;
}
int main() {
cin >> n >> p;
for(int i = 1; i <= n; i++) A[i] = a[i];
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++) {
Read(A[i][j]); A[i][j] %= p;
}
cout << solve();
return 0;
}
同时,矩阵树定理也需要用到行列式。
Exercise:
B2104 矩阵加法
B3615 测测你的矩阵乘法
P1962 斐波那契数列
P1939 【模板】矩阵加速(数列)
P2106 Sam数
P1707 刷题比赛
P2044 [NOI2012] 随机数生成器
P1306 斐波那契公约数
P1349 广义斐波那契数列
P5343 【XR-1】分块
P8944 The Da Vinci Code
P6178 【模板】Matrix-Tree 定理