线性代数学习笔记
1. 基础知识
推荐 3b1b 《线性代数的本质》
1.1.向量
向量的英文叫 vector,就是那个超级好用的 STL 的容器的名字来源。向量可以表示一组元素,如:向量 \(\vec{A}=(a_1,a_2,a_3,...,a_n)\)。
其中向量 \(\vec{A}=(a_1,a_2,a_3,...,a_n)\) 这样的向量被称作行向量。
而形如:
则称作列向量。
1.1.1.向量数量积
两个 \(n\) 维向量 \(\vec{A}=(a_1,a_2,\ldots,a_n)\) 与 \(\vec{B}=(b_1,b_2,\ldots,b_n)\) 的数量积(也称作点积、内积)为
1.2.矩阵
矩阵可以理解为一个 \(n\) 行 \(m\) 列的矩阵。比如:
一般来说,我们主要会用到的矩阵都是方阵,即 \(n=m\) 的矩阵。
1.2.1.矩阵乘法
矩阵乘法只有在前一个矩阵是 \(n\) 行 \(m\) 列,后一个矩阵是 \(m\) 行 \(k\) 列时才有意义。
\(A\) 是一个 \(n\) 行 \(m\) 列的矩阵,\(B\) 是一个 \(m\) 行 \(k\) 列的矩阵,设 \(A \times B=C\),则 \(C\) 是一个 \(n\) 行 \(k\) 列的矩阵。并且对于所有 \(1 \le i \le n,1 \le j \le k\),都有:
矩阵乘法满足结合律,但不满足交换律。
1.2.2.单位矩阵
一个 \(n\) 行 \(n\) 列的单位矩阵 \(I\) 指的是从左上到右下这条对角线上的数都为 \(1\),其他位置都是 \(0\) 的矩阵。
当 \(n=3\) 时:
对于任意一个 \(n\) 行 \(n\) 列的方阵 \(A\),都有:
可以把它视作普通乘法中的 \(1\)。
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;
}
注意矩阵乘法复杂度是 \(O(n^3)\) 的。
1.3.矩阵的应用
1.3.1.矩阵快速幂
我们知道,计算 \(a^n\) 这种幂可以用快速幂来加速,时间复杂度 \(O(\log n)\)。
由于矩阵乘法是满足结合律的,所以对于一个方阵 \(A\),\(A^n\) 也可以用快速幂加速计算。
如果 \(A\) 是一个 \(m\) 行 \(m\) 列的方阵,则快速幂的时间复杂度是 \(O(m^3 \log n)\)。
点击查看代码
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.加速递推
有一些动态规划或者计数问题中,递推的式子都是一样的,这样的递推有时可以用矩阵快速幂来加速。
比如斐波那契的递推式,正常递推是 \(O(n)\) 的:
我们尝试构造一个式子形如:
其中 \(A\) 是方阵,\(x,X\) 都是大小和 \(A\) 行数相同的向量。把矩阵写出来就是:
根据矩阵乘法的定义我们可以的得到:
由于斐波那契的递推式只涉及前两项,我们的目标就是构造一个 \(2 \times 2\) 的矩阵 \(A\) 使得:
所以我们有:
再根据 \(F_{i}=F_{i-1}+F_{i-2}\) 就可以得到:
所以我们就有:
相当于我们只要不断地乘上一个 \(A\),向量便会不断更新,每次更新都可以多求出一个斐波那契数。
即:
这样我们就可以用矩阵快速幂来加速这个过程,时间复杂度 \(O(\log n)\)。
当然,实现中我们可以把 \(x,X\) 都写成一个 \(2 \times 2\) 的方阵,除了第一列都是 \(0\)。
这样递推就是 \(O(\log n)\) 的了。
点击查看代码
#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.高斯消元
高斯消元其实就是解一个 \(n\) 元一次方程组的流程,本质是消元。
以下面的方程组为例:
很明显,这个方程的解就是 \(x_1=x_2=x_3=1\)。
高斯消元会这么做:
把方程组看成一个矩阵:
把矩阵每一行是做一个整体,看做一个向量。我们现在能做的是向量之间的加减,单个向量乘以一个常数。我们的目标是把矩阵变成一个这样的矩阵:
即单位矩阵。
首先我们要把第一行的第一个数变成 \(1\),由于已经是 \(1\) 了,所以不用动。
然后我们用第一行把第二三行的第一个数都变成 \(0\):
类似的,我们把第二行第二个数变成 \(1\),再消去其他行的第二个数:
第三行也是同理:
然后就完事儿了。
代码实现起来也非常简单,我们可以用 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 线性相关与线性无关
对于一组向量 \((v_1, v_2, \dots, v_n)\) 如果存在不全为 \(0\) 的实数 \((a_1, a_2, \dots, a_n)\) 满足 \(\sum_{i=1}^n{a_iv_i}\) 为零向量,则称这一组向量线性相关,否则无关。
1.5.3 生成空间
对于向量集合 \(\{v_1, v_2, \dots, v_n\}\),其生成空间是 \(\{\sum_{i=1}^na_iv_i|a_i \in \mathbb{R}\}\)。
1.5.4 基和维度
如果一个向量集合线性无关且生成空间包括所有向量,则这组向量称为这个空间的一个基,其大小等于这个空间的维度。
1.6 行列式
1.6.1 定义
对于一个 \(n \times n\) 矩阵 \(A\),定义其行列式为:
其中 \(sgn(\pi)\) 定义为当 \(\pi\) 是奇排列时为 \(-1\),偶排列为 \(1\),奇偶排列等于排列中逆序对的个数的奇偶性。
1.6.2 本质
本质上来说就是 \(A\) 作用与 \(n\) 维空间后所有单位向量组成的 \(n\) 维意义下的体积。
有一个推论:对于一个二维平面上 \((0,0),(a,b),(c,d)\) 构成三角形面积等于 \(ad - bc\),如果三点不共线。
1.6.3 运算规则
直接根据定义计算复杂度很高,但是我们可以通过以下规则在 \(O(n^3)\) 解决这个问题。
下面都是对列的讨论,对行同样成立。
规则 1: \(A\) 中 \(n\) 个列向量如果有若干个向量线性相关,则行列式为 \(0\)。
感性理解,行列式是体积,如果凑不出 \(n\) 维空间就只有面积甚至长度,显然无论如何 \(n\) 维意义下的体积都是 \(0\)。
规则 2: \(\det(a, b + c) = \det(a, b) + \det(a,c)\)。
这个可以通过观察定义式得到。
规则 3: 将第 \(i\) 列乘以 \(a\) 后加到第 \(j\) 列,行列式不变。
可以通过规则 1 和规则 2 推出。
规则 4: 交换两列,行列式取反。
规则 5: 某列增大 \(a\) 倍,则行列式也增大 \(a\) 倍。
规则 6: \(\det(A) = \det(A^T)\)。\(A^T\) 是转置矩阵,也就是沿着对角线翻折得到。
规则 7(Binet-Cauchy定理): 如果 \(AB\) 是 \(n \times m\) 的矩阵且 \(n \le m\),则 \(\det(AB) = \sum_{S \sube [m],|S| = n}det(A_S)det(B_S)\),其中 \(A_S\) 表示 \(A\) 中只保留 \(S\) 中的列,\(B_S\) 表示只保留 \(S\) 中的行。
显然有了这些规则就可以用类似高斯消元来做了。
对于任意模数,我们只能够通过辗转相除来代替求逆元的方式计算。
这里给一下 模板题 的代码:
点击查看代码
#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 定理描述
对于一张无向图 \(G=(V,E)\),定义矩阵 \(Q\) 如下:
对于对角线上的每个值 \(Q_{i,i} = \deg_i(G)\)。
对于 \(Q_{i,j}\),如果 \((i,j)\) 没有边,则为 \(0\);否则为 \((i,j)\) 这条边的数量的相反数。
删去 \(Q\) 的第一行第一列得到 \(M_{1,1}\),则 \(G\) 中生成树个数等于 \(\det(M_{1,1})\)。
2.1.2 证明
证明很重要!
首先,我们构造一个 \(n \times m\) 的矩阵 \(E\),其中每一列代表一条边,假设这条边链接 \((u,v)\),则我们将这一列第 \(u\) 个设为 \(1\),第 \(v\) 个设为 \(-1\),其实谁设 \(1\) 谁设 \(-1\) 无所谓,为了方便我们假定 \(u < v\)。对于这一列其余元素全部为 \(0\)。
于是我们先证明第一步: \(Q = E \times E^T\)。
对于 \(Q_{i,i}\),其值等于 \(E\) 的第 \(i\) 行与自己的乘积,于是有多少个 \(\pm1\) 就有多少条边,等于其度数。
对于 \(Q_{i,j}\) 它等于 \(E\) 的第 \(i\) 行与第 \(j\) 行的乘积,如果存在 \((i,j)\) 就会贡献 \(-1\),于是也和 \(Q\) 相等。
接下来,不妨设 \(F\) 表示 \(E\) 去掉第一行的矩阵,我们可以推出 \(M_{1,1} = F \times F^T\)。
根据 Binet-Cauchy 定理和行列式的性质我们得出:
而选取 \(S\) 可以理解为在 \(m\) 条边中选 \(n-1\) 条边,我们要证明当且仅当 \(S\) 是生成树 \(\det(F_S) = \pm 1\),否则为 \(0\)。
如果不是生成树,说明有环,把环的那几列都选出来,然后就会发现全都集中到某一列后这一列会变成 \(0\),于是行列式就是 \(0\)。
如果是生成树,一定可以通过交换变成上三角矩阵,然后行列式就是对角线的乘积了,必须是 \(\pm 1\)。
于是就证明了这个定理。
2.1.3 带权版本
如果边带权,要求出所有生成树权值的积的总和,我们依然可以用这个定理。
在证明过程中,我们把 \(E\) 中设为 \(1\) 和 \(-1\) 改为设成 \(w_e^{0.5}\) 和 \(-w_e^{0.5}\),这样我们依然可以求出 \(F\),然后求出 \(M_{1,1}\) 最后求出行列式即可。证明与上面类似。
具体,\(Q_{i,i}\) 变成所有邻边的权值和,\(Q_{i,j}\) 也是负的边权和。
2.1.4 内向树与外向树
这个定理也可以用来求无向图中内向生成树与外向生成树的个数。
外向生成树:对于转置矩阵,只用入点为 \(1\),最后的 \(Q\) 中对角线上只统计入度,\(Q_{i,j}\) 只统计 \(i \to j\) 的边。
内向生成树:只用出点,统计出度。
同样都可以推广到带权版本。
这里给一下 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] 重建
带权版本,我们边权换成 \(\frac{p_i}{1 - p_i}\),然后最后答案再乘上 \((1-p_i)\) 的积即可。
CF917D Stranger Trees
恰有 \(k\) 条黑白生成树计数。
我们考虑借助多项式来做。我们依然是将 \(E\) 中每条边的两个值改一下,如果是黑边就改成 \(\pm x^{0.5}\),否则就是 \(\pm 1\),显然最后 \(\det(M_{1,1})\) 是一个 \(n-1\) 次多项式,我们需要知道 \(x^k\) 的系数。如果直接 FFT 等科技会比较慢,所以考虑插值,只用将 \(n\) 个值带进去算一下,由于需要求系数,可以用高斯消元来求解。
考虑到存在 \(n-1\) 条黑白的生成树,这意味这这个多项式恰好是 \(n-1\) 次,所以高斯消元不会存在自由元。
时间复杂度 \(O(n^4)\)。
牛客网 计数
要求选出边权和是 \(k\) 的倍数,求有多少种选法,模质数 \(P\),满足 \(P \equiv 1 \pmod k\)。
还是需要借助多项式,将两个值变成 \(\pm x^{\frac{w_e}{2}}\),最后看所有 \(k\) 的倍数的系数之和。
但是这样时间复杂度会比较高,因为多项式次数比较高,于是我们考虑如何利用 \(P\) 的条件。
我们发现,如果答案是 \(f(x) = \sum_{i=0}^na_ix^i\),那么我们构造 \(F(x)\) 使得 \(F(x) = \sum_{i=0}^{k - 1}\sum_{i + jk \le n}a_{i + jk} x^i\),对余数分类,这样我们就可以只求常数项。
我们为了插值,找到干个满足 \(x^k \equiv 1 \pmod p\) 的 \(x\) 来算,具体可以找原根,这样 \(F(x) = f(x)\),然后我们就只用求 \(k\) 次就可以找到足够我们算出常数项的了。
P4336 [SHOI2016] 黑暗前的幻想乡
子集反演,设 \(g(S)\) 表示不用 \(S\) 中的,其它随便,\(f(S)\) 表示不用 \(S\) 中的,其它必须用。这样就可以得到 \(g(S) = \sum_{T \supe S}f(T)\),反演一下即可求出 \(f(S)\),而我们需要知道 \(f(\empty)\),就等于 \(\sum_{S}(-1)^{|S|}g(S)\)。
而计算 \(g\) 只用把 \(S\) 中的边去掉,剩下的边的生成树计数即可。时间复杂度 \(O(n^32^n)\)。
CF578F Mirror Box
神题。
考虑将网格图的格点视为点然后黑白染色。发现条件成立的等价条件是黑点或白点形成一棵生成树,另一颜色的点连上所有可以连的边。
然后就可以缩点然后分别求黑点和白点的生成树个数即可。