浅谈线性代数合集
说明
本文收录了 NOIP 级别的部分常用线性代数知识。
由于某些原因,小部分例题不放代码。
更新日志:
- 2026.3.7:更新矩阵;
- 2026.3.26-4.14:更新高斯消元(中间作者有点忙);
- 2026.4.15:更新线性基;
- 2026.4.19 11:07:排版检查,整改错别字。
矩阵
Part 0:前置知识
- 基础数学。
Part 1:矩阵的基本定义和运算
1.1:矩阵的基本定义
- 矩阵:由
m \times n 个数排成的m 行n 列的数表,记为A_{m \times n} ,其中第i 行第j 列的元素记作A_{i, j} ; - 方阵:满足
m = n 的矩阵(用的很多); - 特殊矩阵:
- 零矩阵:满足
A_{i, j} 均为0 ,常被记作O ; - 单位矩阵(
n = m ):主对角线(左上到右下)的元素均为1 (即A_{i, i} = 1 ),其他元素均为0 ,常被记作I 。
- 零矩阵:满足
其中,单位矩阵有一个性质:对于任意方阵
相信细心的你可以发现,其实线性代数中的
1.2:矩阵的基本运算
矩阵加法:
将每一个矩阵元素相加,即
例子:
矩阵数乘:
将每一个矩阵元素乘以该数,即
例子:
矩阵乘法(
设矩阵
例子:
矩阵乘法满足结合律,即
::::success[Code]
struct Matrix {
ll mtx[kMaxN][kMaxN];
Matrix operator* (const Matrix &b) const {
Matrix c;
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= k; ++ j) {
c.mtx[i][j] = 0;
for (int l = 1; l <= m; ++ l) {
c.mtx[i][j] += mtx[i][l] * b.mtx[l][j];
}
}
}
return c;
}
}
::::
作者建议,写矩阵乘法时和矩阵的结构体封装。
Part 2:矩阵快速幂算法
由于矩阵乘法满足结合律,我们便把快速幂的思想推广到矩阵当中。
借助快速幂的思想,我们可以推出结论:
对于方阵
于是,我们将初始矩阵设为单位矩阵
::::success[Code]
struct Matrix {
ll mtx[kMaxN][kMaxN];
Matrix operator* (const Matrix &b) const {
Matrix c;
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= n; ++ j) {
c.mtx[i][j] = 0;
for (int l = 1; l <= n; ++ l) {
c.mtx[i][j] = (c.mtx[i][j] + mtx[i][l] * b.mtx[l][j] % kMod) % kMod;
}
}
}
return c;
}
};
Matrix Qpow(Matrix a, ll k) {
Matrix ans;
for (int i = 1; i <= n; ++ i) {
ans.mtx[i][i] = 1;
}
for (; k; k >>= 1) {
if (k & 1) {
ans = ans * a;
}
a = a * a;
}
return ans;
}
::::
设方阵的大小为
Part 3:矩阵加速递推
我们在做递推或 DP 类题目时,如果答案的
而矩阵是一个好东西,它可以大幅加速你的递推效率,一般会到
为了更好的讲解,作者就拿斐波那契数列为例,它的递推式如下:
那么怎么用矩阵加速递推?在这之前,我们先要了解一个东西。
其实,矩阵的引入来自于线性方程组,而矩阵恰好体现了一种对数据打包处理的思想。
举个例子:
根据矩阵乘法的原理,我们将上述方程组写成矩阵乘法的形式:
虽然这些跟高斯消元有关系,但这跟矩阵加速递推的原理密切相关。
好了,回到正题。我们希望能从矩阵
现在你就可以发现,这跟线性方程组的矩阵乘法形式一模一样!
那么考虑写出
把系数提出来,就可以得到初始矩阵
现在,考虑将此矩阵递推式推广成
带入
现在,线性递推的时间复杂度竟被神奇的降为
Part 4:例题
B2105 矩阵乘法 / P3390 【模板】矩阵快速幂
模板题,直接套就行了。
注意 P3390 要取模。
P1962 斐波那契数列
就是刚刚的例子,直接按照式子加上上面的模板即可。
::::success[Code]
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 110;
const ll kMod = 1e9 + 7;
ll n;
struct Matrix {
ll mtx[kMaxN][kMaxN];
Matrix operator* (const Matrix &b) const {
Matrix c;
for (int i = 1; i <= 2; ++ i) {
for (int j = 1; j <= 2; ++ j) {
c.mtx[i][j] = 0;
for (int l = 1; l <= 2; ++ l) {
c.mtx[i][j] = (c.mtx[i][j] + mtx[i][l] * b.mtx[l][j] % kMod) % kMod;
}
}
}
return c;
}
} f;
Matrix Qpow(Matrix a, ll k) {
Matrix ans;
for (int i = 1; i <= 2; ++ i) {
ans.mtx[i][i] = 1;
}
for (; k; k >>= 1) {
if (k & 1) {
ans = ans * a;
}
a = a * a;
}
return ans;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n;
f.mtx[1][1] = 1, f.mtx[1][2] = 1;
f.mtx[2][1] = 1, f.mtx[2][2] = 0;
cout << Qpow(f, n).mtx[2][1] << '\n';
return 0;
}
::::
P4838 P哥破解密码
这种题是见的比较多的,一般矩阵优化递推常常跟 DP 一起融合。
显然,令
那么,容易得到转移方程(可以自己推一下):
变一下形式:
那么初始矩阵
值得注意的是,答案需要将最终矩阵第一行的元素全部加起来,因为答案为
多测记得清空!
P2886 [USACO07NOV] Cow Relays G
挺有趣的一道题。
首先,我们观察一下 Floyd 的代码:
for (int k = 1; k <= n; ++ k) {
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= n; ++ j) {
f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
}
}
}
是不是有点像矩阵乘法!
再看,发现有边的条数的限制。那么我们思考:假设我们有两个矩阵,如果其中一个矩阵代表恰好经过
怎么合并?我们魔改一下矩阵乘法:
这时候有人说,那不就转移
但是,结合矩阵乘法的雷霆复杂度,肯定会卡飞你。
那怎么办呢?我们复习一下矩阵快速幂的代码,里面的矩阵乘法是不是可以改为我们合并的过程呢?
这样我们转移
最后说一句,点的编号需要离散化。
::::success[Code]
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 2020;
int n, t, s, e, cnt, c[kMaxN * kMaxN];
struct Matrix {
int a[kMaxN][kMaxN];
Matrix operator* (const Matrix &b) const {
Matrix c;
memset(c.a, 0x3f3f3f3f, sizeof c.a);
for (int k = 1; k <= cnt; ++ k) {
for (int i = 1; i <= cnt; ++ i) {
for (int j = 1; j <= cnt; ++ j) {
c.a[i][j] = min(c.a[i][j], a[i][k] + b.a[k][j]);
}
}
}
return c;
}
} dis, ans;
void Qpow() {
ans = dis;
-- n;
for (; n; n >>= 1) {
if (n & 1) {
ans = ans * dis;
}
dis = dis * dis;
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> t >> s >> e;
memset(dis.a, 0x3f3f3f3f, sizeof dis.a);
for (int i = 1, u, v, w; i <= t; ++ i) {
cin >> w >> u >> v;
if (!c[u]) {
c[u] = ++ cnt;
}
if (!c[v]) {
c[v] = ++ cnt;
}
dis.a[c[u]][c[v]] = dis.a[c[v]][c[u]] = w;
}
Qpow();
cout << ans.a[c[s]][c[e]] << '\n';
return 0;
}
::::
高斯消元
Part 0:前置知识
由
- 系数:
a_{i, j} 表示第i 个方程中x_j 的系数; - 常数项:
b_i 表示第i 个方程的右侧参数; - 解:一组满足所有方程
x_1, x_2, \cdots, x_n 的值。
Part 1:高斯消元的基础概念
1.1 增广矩阵
其实我们研究线性方程组本质上关注系数和常数项。为了简化,我们把系数和常数项组成增广矩阵,左侧是系数,右侧是常数项:
例如方程组:
对应的增广矩阵为:
1.2:初等行变换
高斯消元的本质是对增广矩阵做不改变方程组解的变换,即初等行变换(也有等价的初等列变换),有以下三种:
- 交换两行:交换矩阵的第
i 行和第j 行,记为R_i \leftrightarrow R_j ; - 某一行乘非零常数:第
i 行所有元素乘非零常数k ,记为R_i \to R_i \times k ; - 某一行的
k 倍加到另一行:第j 行的k 倍加到第i 行,记为R_i \to R_i + k \times R_j 。
可以证明初等行变换不改变方程组的解。
1.3:行阶梯形矩阵
高斯消元的目标是将增广矩阵化为行阶梯形矩阵(每一行的主元都在上一行主元的右边),形如:
这样我们就可以从最后一行往上回代求解。
1.4:矩阵的秩
将矩阵化简为阶梯矩阵后,非零行的数量称之为矩阵的行秩(英文:rank)。
性质:行秩
矩阵的秩是判断线性方程组是否有解的重要工具。
情况 1:无解
系数矩阵的秩
情况 2:无数解
增广矩阵的秩
情况 3:唯一解
系数矩阵的秩
读者可以自证锻炼思维。
Part 2:基础高斯消元的原理
以
2.1:前向消元
目标:将增广矩阵化为上三角矩阵。
我们以三元方程组为例。
设原增广矩阵为:
第一步:处理第一列。
- 选主元:找第一列中,从第一行开始往下绝对值最大的元素(可以避免浮点数精度误差),我们假设主元在第一行(
a_{1, 1} \neq 0 )。 - 消去第一列下方的元素:
- 对第二行:计算系数
k_2 = \frac{a_{2, 2}}{a_{1, 1}} ,执行R_2 \to k_2 \times R_1 ,则第二行第一列变为a_{2, 1} - k_2 \cdot a_{1, 1} ; - 对第三行:计算系数
k_3 = \frac{a_{3, 1}}{a_{1, 1}} ,执行R_3 \to k_3 \times R_1 ,则第三行第一列变为0 。
- 对第二行:计算系数
此时,矩阵变为:
第二步:处理第二列。
- 选主元:找第二列中,从第二行开始往下绝对值最大的元素(
a'_{2, 2} \neq 0 )。 - 消去第二列下方的元素:
- 对第三行:计算系数
k_3 = \frac{a'_{3, 2}}{a'_{2, 2}} ,执行R_3 \to k_3' \times R_2 ,则第三行第一列变为0 。
- 对第三行:计算系数
此时,矩阵变为:
2.2:回代求解
目标:从最后一行开始,依次计算每一个未知数的值。
求
求
求
Part 3:高斯-约旦消元法
上三角矩阵进一步优化,变成最简行阶梯矩阵可以使高斯消元的过程不需要回代,这就是高斯-约旦消元法。
具体来讲,就是每次在第行确定最大系数
::::success[Code]
const double eps = 1e-8;
void Gauss() {
for (int i = 1, mxr; i <= n; ++ i) {
mxr = i;
for (int j = i + 1; j <= n; ++ j) {
if (abs(a[j][i]) > abs(a[mxr][i])) {
mxr = j;
}
}
for (int j = i + 1; j <= n + 1; ++ j) {
swap(a[i][j], a[mxr][j]);
}
if (abs(a[i][i]) < eps) {
continue;
}
double div = a[i][i];
for (int j = 1; j <= n + 1; ++ j) {
a[i][j] /= div;
}
for (int j = 1; j <= n; ++ j) {
if (j != i) {
double rate = a[j][i];
for (int k = i; k <= n + 1; ++ k) {
a[j][k] -= rate * a[i][k];
}
}
}
}
}
::::
Part 3:例题
P3389 【模板】高斯消元法 / P2455 [SDOI2006] 线性方程组
模板题,套模板即可。
当存在自由元(无穷组解)时,有一个
当无解时,有一个
P5027 Barracuda
极水,枚举错误的是那一次称重,剩下的列方程高斯消元即可。
P4035 [JSOI2008] 球形空间产生器
根据提示,可以得到球面上的任何一点到球心距离相等。
所以考虑设球心坐标为
再结合题目给的公式,列出以下方程:
相邻两个方程作差,得:
移项,得:
这不就是高斯消元板子吗,直接暴力套板子!
线性基
Part 0:前置知识
- 异或(XOR)。
Part 1:线性基的基本概念
1.1:线性基解决的问题
在算法竞赛中,异或是高频考点,而线性基是解决所有子集异或问题的重要工具。
如果说,现在给定
但是,线性基可以将
1.2:线性相关与无关(异或意义下)
定义:
- 线性无关:一组数里,任何一个数都不能被其他数异或出来,即没有多余的数;
- 线性相关:一组数里,至少有一个数能被其他数异或出来,即存在数是多余的。
Part 2:线性基的构造
2.1:线性基的性质
对于一个整数集合
2.2:线性基的构造规则
虽然我们可以使用高斯消元来构造出线性基,但是这样时间复杂度较高,我们考虑使用构造。
假设线性基有
接下来,我们按照这些流程插入数字
- 从最高位
n - 1 到最低位0 遍历x 的二进制位; - 找到
x 的最高非零位k ; - 如果
p_k = 0 ,直接将x 存入p_k ,结束; - 如果
p_k \neq 0 ,令x = x \oplus p_k (也就是消去x 的第k 位),回到步骤2 ; - 如果
x 最终变为0 ,说明x 对线性基没有贡献。 - 如果你想求标准线性基,那么你需要对于求取的所有
p_i ,从低位到高位枚举,用低位的1 消去所有高位的1 ,确保每个p_i 只有唯一的1 。
::::success[Code]
void Ins(ll x) { // 插入数字 x
for (int i = 63; i >= 0; -- i) {
if (x >> i) {
if (!p[i]) {
p[i] = x;
break;
} else {
x ^= p[i];
}
}
}
}
void Rebuild() { // 重构线性基
for (int i = 63; i >= 0; -- i) {
for (int j = i - 1; j >= 0; -- j) {
if (p[i] & (1ll << j)) {
p[i] ^= p[j];
}
}
}
for (int i = 0; i <= 63; ++ i) {
if (p[i]) {
d[cnt ++] = p[i];
}
}
}
::::
正确性证明请读者自证(比较简单)。
Part 3:例题
P3812 【模板】线性基
模板题,多一个求最大值。
想求最大异或值,就从最高位往低位一个个看线性基里对应这一位的数。如果把当前答案跟这个数异或一下,结果变得更大,那就更新答案。因为低位怎么变都影响不了已经确定的高位,所以这么做肯定是最优的。
P4570 [BJWC2011] 元素 / P3265 [JLOI2015] 装备购买
这两道题有着相似性,我们首先解决第一道题。
正确方法是先按魔力值排序,然后一个个插入,能插入的就把答案加上此矿石的魔力值。
怎么证明?
其实比较简单。因为对于两个同时可以存进第
于是就证完了。
再来解决第二题。
我们发现,这一题只是把第一问的数字改成了向量。
在异或线性基中,我们通过异或(XOR)把最高位消掉。那么,在实数线性基中,我们便可以使用四则运算抵消最高位,即:
于是,插入的代码就得出来了。
最后不要忘记这是向量!
::::success[Code]
void Ins(Node x) {
for (int i = m - 1; i >= 0; -- i) {
if (x[i]) {
if (!p[i][i]) {
p[i] = x;
break;
}
x = x - p[i] * x.z[i] / p[i][i];
}
}
}
::::