线性代数学习笔记
1. 基础知识
推荐 3b1b 《线性代数的本质》
1.1.向量
向量的英文叫 vector,就是那个超级好用的 STL 的容器的名字来源。向量可以表示一组元素,如:向量 。
其中向量 这样的向量被称作行向量。
而形如:
则称作列向量。
1.1.1.向量数量积
两个 维向量 与 的数量积(也称作点积、内积)为
1.2.矩阵
矩阵可以理解为一个 行 列的矩阵。比如:
一般来说,我们主要会用到的矩阵都是方阵,即 的矩阵。
1.2.1.矩阵乘法
矩阵乘法只有在前一个矩阵是 行 列,后一个矩阵是 行 列时才有意义。
是一个 行 列的矩阵, 是一个 行 列的矩阵,设 ,则 是一个 行 列的矩阵。并且对于所有 ,都有:
矩阵乘法满足结合律,但不满足交换律。
1.2.2.单位矩阵
一个 行 列的单位矩阵 指的是从左上到右下这条对角线上的数都为 ,其他位置都是 的矩阵。
当 时:
对于任意一个 行 列的方阵 ,都有:
可以把它视作普通乘法中的 。
1.2.3.代码实现
我们可以用一个结构体来存矩阵,具体可以用 vector 或者二维数组来存。
点击查看代码
struct Mtr {
int n;
vector<vector<long long> > m;
Mtr () {
n = 0;
}
Mtr (int x) {
n = x;
m = vector<vector<long long> >(n, vector<long long>(n, 0));
}
};
对于矩阵乘法,我们可以通过重载运算符来实现:
点击查看代码
Mtr operator*(Mtr x, Mtr y) {
Mtr ans = Mtr(x.n);
for (int i = 0; i < ans.n; i++)
for (int j = 0; j < ans.n; j++)
for (int k = 0; k < ans.n; k++)
ans.m[i][j] += x.m[i][k] * y.m[k][j] % mod, ans.m[i][j] %= mod;
return ans;
}
注意矩阵乘法复杂度是 的。
1.3.矩阵的应用
1.3.1.矩阵快速幂
我们知道,计算 这种幂可以用快速幂来加速,时间复杂度 。
由于矩阵乘法是满足结合律的,所以对于一个方阵 , 也可以用快速幂加速计算。
如果 是一个 行 列的方阵,则快速幂的时间复杂度是 。
点击查看代码
Mtr fpow(Mtr x, long long b) {
if (b == 0) {
Mtr ans = Mtr(x.n);
for (int i = 0; i < ans.n; i++)
for (int j = 0; j < ans.n; j++)
ans.m[i][j] = (i == j);
return ans;
}
Mtr t = fpow(x, b / 2);
return (b % 2 == 1) ? (t * t * x) : (t * t);
}
1.3.2.加速递推
有一些动态规划或者计数问题中,递推的式子都是一样的,这样的递推有时可以用矩阵快速幂来加速。
比如斐波那契的递推式,正常递推是 的:
我们尝试构造一个式子形如:
其中 是方阵, 都是大小和 行数相同的向量。把矩阵写出来就是:
根据矩阵乘法的定义我们可以的得到:
由于斐波那契的递推式只涉及前两项,我们的目标就是构造一个 的矩阵 使得:
所以我们有:
再根据 就可以得到:
所以我们就有:
相当于我们只要不断地乘上一个 ,向量便会不断更新,每次更新都可以多求出一个斐波那契数。
即:
这样我们就可以用矩阵快速幂来加速这个过程,时间复杂度 。
当然,实现中我们可以把 都写成一个 的方阵,除了第一列都是 。
这样递推就是 的了。
点击查看代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int mod = 1e9 + 7;
struct Mtr {
int n;
vector<vector<long long> > m;
Mtr () {
n = 0;
}
Mtr (int x) {
n = x;
m = vector<vector<long long> >(n, vector<long long>(n, 0));
}
};
Mtr operator*(Mtr x, Mtr y) {
Mtr ans = Mtr(x.n);
for (int i = 0; i < ans.n; i++)
for (int j = 0; j < ans.n; j++)
for (int k = 0; k < ans.n; k++)
ans.m[i][j] += x.m[i][k] * y.m[k][j] % mod, ans.m[i][j] %= mod;
return ans;
}
Mtr fpow(Mtr x, long long b) {
if (b == 0) {
Mtr ans = Mtr(x.n);
for (int i = 0; i < ans.n; i++)
for (int j = 0; j < ans.n; j++)
ans.m[i][j] = (i == j);
return ans;
}
Mtr t = fpow(x, b / 2);
return (b % 2 == 1) ? (t * t * x) : (t * t);
}
long long n;
Mtr A;
int main() {
cin >> n;
Mtr A = Mtr(2), B = Mtr(2);
A.m[0][0] = 0, A.m[0][1] = 1;
A.m[1][0] = 1, A.m[1][1] = 1;
B.m[0][0] = 1, B.m[0][1] = 0;
B.m[1][0] = 1, B.m[1][1] = 0;
if (n <= 2)
cout << 1 << endl;
else
cout << (fpow(A, n - 2) * B).m[1][0] << endl;
return 0;
}
1.4.高斯消元
高斯消元其实就是解一个 元一次方程组的流程,本质是消元。
以下面的方程组为例:
很明显,这个方程的解就是 。
高斯消元会这么做:
把方程组看成一个矩阵:
把矩阵每一行是做一个整体,看做一个向量。我们现在能做的是向量之间的加减,单个向量乘以一个常数。我们的目标是把矩阵变成一个这样的矩阵:
即单位矩阵。
首先我们要把第一行的第一个数变成 ,由于已经是 了,所以不用动。
然后我们用第一行把第二三行的第一个数都变成 :
类似的,我们把第二行第二个数变成 ,再消去其他行的第二个数:
第三行也是同理:
然后就完事儿了。
代码实现起来也非常简单,我们可以用 vector 来存向量,重载运算符。
点击查看代码
#include <iostream>
#include <cstdio>
#include <cmath>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
int n;
vector<double> a[105];
vector<double> operator+(vector<double> x, vector<double> y) {
vector<double> ans((int)x.size());
for (int i = 0; i < (int)ans.size(); i++)
ans[i] = x[i] + y[i];
return ans;
}
vector<double> operator-(vector<double> x, vector<double> y) {
vector<double> ans((int)x.size());
for (int i = 0; i < (int)ans.size(); i++)
ans[i] = x[i] - y[i];
return ans;
}
vector<double> operator*(vector<double> x, double y) {
vector<double> ans((int)x.size());
for (int i = 0; i < (int)ans.size(); i++)
ans[i] = x[i] * y;
return ans;
}
bool Gauss() {
for (int i = 0; i < n; i++) {
if (abs(a[i][i]) < 1e-9)
return false;
a[i] = a[i] * (1.0 / a[i][i]);
for (int j = 0; j < n; j++)
if (i != j)
a[j] = a[j] - a[i] * a[j][i];
}
return true;
}
int main() {
cin >> n;
for (int i = 0; i < n; i++) {
a[i] = vector<double>(n + 1, 0);
for (int j = 0; j <= n; j++)
cin >> a[i][j];
}
if (!Gauss())
cout << "No Solution" << endl;
else {
for (int i = 0; i < n; i++)
printf("%.2lf\n", a[i][n]);
}
return 0;
}
1.5 线性代数的本质
以下讨论的都是实数域下的。
1.5.1 向量和矩阵
向量其实就是一条从原点出发的箭头,而矩阵描述的是一个坐标变化,矩阵乘法本质上是坐标变化的复合。
1.5.2 线性相关与线性无关
对于一组向量 如果存在不全为 的实数 满足 为零向量,则称这一组向量线性相关,否则无关。
1.5.3 生成空间
对于向量集合 ,其生成空间是 。
1.5.4 基和维度
如果一个向量集合线性无关且生成空间包括所有向量,则这组向量称为这个空间的一个基,其大小等于这个空间的维度。
1.6 行列式
1.6.1 定义
对于一个 矩阵 ,定义其行列式为:
其中 定义为当 是奇排列时为 ,偶排列为 ,奇偶排列等于排列中逆序对的个数的奇偶性。
1.6.2 本质
本质上来说就是 作用与 维空间后所有单位向量组成的 维意义下的体积。
有一个推论:对于一个二维平面上 构成三角形面积等于 ,如果三点不共线。
1.6.3 运算规则
直接根据定义计算复杂度很高,但是我们可以通过以下规则在 解决这个问题。
下面都是对列的讨论,对行同样成立。
规则 1: 中 个列向量如果有若干个向量线性相关,则行列式为 。
感性理解,行列式是体积,如果凑不出 维空间就只有面积甚至长度,显然无论如何 维意义下的体积都是 。
规则 2: 。
这个可以通过观察定义式得到。
规则 3: 将第 列乘以 后加到第 列,行列式不变。
可以通过规则 1 和规则 2 推出。
规则 4: 交换两列,行列式取反。
规则 5: 某列增大 倍,则行列式也增大 倍。
规则 6: 。 是转置矩阵,也就是沿着对角线翻折得到。
规则 7(Binet-Cauchy定理): 如果 是 的矩阵且 ,则 ,其中 表示 中只保留 中的列, 表示只保留 中的行。
显然有了这些规则就可以用类似高斯消元来做了。
对于任意模数,我们只能够通过辗转相除来代替求逆元的方式计算。
这里给一下 模板题 的代码:
点击查看代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 605;
int mod;
int n;
int a[N][N] = {{0}};
int DET() {
int ans = 1;
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
while (a[i][i]) {
int x = a[j][i] / a[i][i];
for (int k = i; k <= n; k++)
a[j][k] = (a[j][k] - 1ll * x * a[i][k] % mod + mod) % mod;
swap(a[i], a[j]), ans = (mod - ans) % mod;
}
swap(a[i], a[j]), ans = (mod - ans) % mod;
}
ans = 1ll * ans * a[i][i] % mod;
}
return ans;
}
int main() {
cin >> n >> mod;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
cin >> a[i][j];
cout << DET() << endl;
return 0;
}
2 矩阵树定理(Kirchhoff's matrix tree theorem)
主要与生成树计数相关。
2.1 定理
2.1.1 定理描述
对于一张无向图 ,定义矩阵 如下:
对于对角线上的每个值 。
对于 ,如果 没有边,则为 ;否则为 这条边的数量的相反数。
删去 的第一行第一列得到 ,则 中生成树个数等于 。
2.1.2 证明
证明很重要!
首先,我们构造一个 的矩阵 ,其中每一列代表一条边,假设这条边链接 ,则我们将这一列第 个设为 ,第 个设为 ,其实谁设 谁设 无所谓,为了方便我们假定 。对于这一列其余元素全部为 。
于是我们先证明第一步: 。
对于 ,其值等于 的第 行与自己的乘积,于是有多少个 就有多少条边,等于其度数。
对于 它等于 的第 行与第 行的乘积,如果存在 就会贡献 ,于是也和 相等。
接下来,不妨设 表示 去掉第一行的矩阵,我们可以推出 。
根据 Binet-Cauchy 定理和行列式的性质我们得出:
而选取 可以理解为在 条边中选 条边,我们要证明当且仅当 是生成树 ,否则为 。
如果不是生成树,说明有环,把环的那几列都选出来,然后就会发现全都集中到某一列后这一列会变成 ,于是行列式就是 。
如果是生成树,一定可以通过交换变成上三角矩阵,然后行列式就是对角线的乘积了,必须是 。
于是就证明了这个定理。
2.1.3 带权版本
如果边带权,要求出所有生成树权值的积的总和,我们依然可以用这个定理。
在证明过程中,我们把 中设为 和 改为设成 和 ,这样我们依然可以求出 ,然后求出 最后求出行列式即可。证明与上面类似。
具体, 变成所有邻边的权值和, 也是负的边权和。
2.1.4 内向树与外向树
这个定理也可以用来求无向图中内向生成树与外向生成树的个数。
外向生成树:对于转置矩阵,只用入点为 ,最后的 中对角线上只统计入度, 只统计 的边。
内向生成树:只用出点,统计出度。
同样都可以推广到带权版本。
这里给一下 P6178 【模板】Matrix-Tree 定理,这个是外向树和无向图的情况:
点击查看代码
#include <iostream>
#include <vector>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 305;
const int mod = 1e9 + 7;
int n, m, T, a[N][N] = {{0}};
int DET() {
int ans = 1;
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
while (a[i][i]) {
int x = a[j][i] / a[i][i];
for (int k = i; k <= n; k++)
a[j][k] = (a[j][k] - 1ll * x * a[i][k] % mod + mod) % mod;
swap(a[i], a[j]), ans = (mod - ans) % mod;
}
swap(a[i], a[j]), ans = (mod - ans) % mod;
}
ans = 1ll * ans * a[i][i] % mod;
}
return ans;
}
int main() {
cin >> n >> m >> T;
for (int i = 1, u, v, w; i <= m; i++) {
cin >> u >> v >> w;
u = n - u + 1, v = n - v + 1;
if (T == 0) {
a[u][u] = (a[u][u] + w) % mod;
a[v][v] = (a[v][v] + w) % mod;
a[u][v] = (a[u][v] - w + mod) % mod;
a[v][u] = (a[v][u] - w + mod) % mod;
}
else {
a[v][v] = (a[v][v] + w) % mod;
a[u][v] = (a[u][v] - w + mod) % mod;
}
}
--n;
cout << DET() << endl;
return 0;
}
对于内向图,模板是 P4455 [CQOI2018] 社交网络
切记矩阵树定理需要删掉一行再计算行列式!!!!!!!!!!!!!!!!!!
2.2 例题
P3317 [SDOI2014] 重建
带权版本,我们边权换成 ,然后最后答案再乘上 的积即可。
CF917D Stranger Trees
恰有 条黑白生成树计数。
我们考虑借助多项式来做。我们依然是将 中每条边的两个值改一下,如果是黑边就改成 ,否则就是 ,显然最后 是一个 次多项式,我们需要知道 的系数。如果直接 FFT 等科技会比较慢,所以考虑插值,只用将 个值带进去算一下,由于需要求系数,可以用高斯消元来求解。
考虑到存在 条黑白的生成树,这意味这这个多项式恰好是 次,所以高斯消元不会存在自由元。
时间复杂度 。
牛客网 计数
要求选出边权和是 的倍数,求有多少种选法,模质数 ,满足 。
还是需要借助多项式,将两个值变成 ,最后看所有 的倍数的系数之和。
但是这样时间复杂度会比较高,因为多项式次数比较高,于是我们考虑如何利用 的条件。
我们发现,如果答案是 ,那么我们构造 使得 ,对余数分类,这样我们就可以只求常数项。
我们为了插值,找到干个满足 的 来算,具体可以找原根,这样 ,然后我们就只用求 次就可以找到足够我们算出常数项的了。
P4336 [SHOI2016] 黑暗前的幻想乡
子集反演,设 表示不用 中的,其它随便, 表示不用 中的,其它必须用。这样就可以得到 ,反演一下即可求出 ,而我们需要知道 ,就等于 。
而计算 只用把 中的边去掉,剩下的边的生成树计数即可。时间复杂度 。
CF578F Mirror Box
神题。
考虑将网格图的格点视为点然后黑白染色。发现条件成立的等价条件是黑点或白点形成一棵生成树,另一颜色的点连上所有可以连的边。
然后就可以缩点然后分别求黑点和白点的生成树个数即可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】