线性代数
文章目录
一、高斯-约旦消元
1. 求解线性方程组
约旦消元法的精度更好、代码更简单,没有回带的过程
-
选择一个尚未被选过的未知数作为主元,选择一个包含这个主元的方程
-
将这个方程主元的系数化为1
-
通过加减消元,消掉其它所有方程中的这个未知数
-
重复以上步骤,直到把每一行都变成只有一项有系数
给出一道例题:P3389 【模板】高斯消元法 - 洛谷
算法说明:
高斯约旦消元是一列一列来处理的,
对于第 i i i 列,其前 i − 1 i-1 i−1 列除了对角线上的元为 1 1 1 ,剩下的元均为 0 0 0 ,
让这一列的数同乘一个系数,使得 m a t [ i ] [ i ] mat[i][i] mat[i][i] 为 1 1 1 ,
然后对 1 ∼ n 1\sim n 1∼n 行用第 i i i 行来减,最后这一列除了 m a t [ i ] [ i ] mat[i][i] mat[i][i] 为 1 1 1 ,剩下值均为 0 0 0
循环下去,最终的解存储在 m a t [ i ] [ n + 1 ] mat[i][n+1] mat[i][n+1] 中
2. 计算行列式
计算行列式则相对要简单一些,化成上上三角阵后,对对角线进行求积即可
给出一道例题: P7112 【模板】行列式求值 - 洛谷
算法说明:
本题是一个整数矩阵,在消元过程中有些不一样的,
题目不能保证逆元的存在性,因此采用了辗转相除法来解决本题
算法也是一列一列的来处理,处理到第 i i i 列时,前 i − 1 i-1 i−1 列已变成上三角阵了
利用辗转相除法,对第 i ∼ n i \sim n i∼n 行进行辗转相除,使得 i + 1 ∼ n i+1\sim n i+1∼n 行的 m a t [ σ ] [ i ] mat[\sigma][i] mat[σ][i] 均为 0 0 0 ,即第 i i i 列也变成了上三角阵,然后再 a n s ∗ = m a t [ i ] [ i ] ans*=mat[i][i] ans∗=mat[i][i] 即可
3. 矩阵求逆
矩阵求逆和求解线性方程挺像,也采用高斯约旦消元,一列一列的消,最后左边是单位阵,右边是逆矩阵,即 [ A , I ] ⇒ [ I , A − 1 ] [A,I] \rArr [I,A^{-1}] [A,I]⇒[I,A−1]
给出一道例题: P4783 【模板】矩阵求逆 - 洛谷
算法说明:
其实思路和“求解线性方程组”的思路几乎一样,不过这里的逆元不再是直接除了,而是利用快速幂来获得逆元
4. 算法比较
求解线性方程组 | 计算行列式 | 矩阵求逆 | |
---|---|---|---|
需要消出 I I I | 是 | 否 | 是 |
是高斯约旦 | 是 | 否 | 是 |
答案存储至 | 矩阵 | 数 | 矩阵 |
i n t int int 型逆元 | 快速幂 | 辗转相除 | 快速幂 |
f l o a t float float 型逆元 | 分数 | 分数 | 分数 |
求解线性方程组( int 型)
#include <iostream>
#define int long long
using namespace std;
const int maxn = 111;
const int mod = 1e9 + 7;
int mat[maxn][maxn];
int quickpow(int a, int b) {
int ans = 1;
while (b) {
if (b & 1) ans = ans * a % mod;
a = a * a % mod, b >>= 1;
}
return ans;
}
bool gauss(int n) {
for (int i = 1; i <= n; i++) {
int pre = i;
for (int j = i + 1; j <= n; j++)
if (mat[j][i] > mat[pre][i]) pre = j;
if (pre != i) swap(mat[i], mat[pre]);
if (!mat[i][i]) return false;
for (int j = n + 1; j >= i; j--) // 循环顺序不可改
mat[i][j] = mat[i][j] * quickpow(mat[i][i], mod - 2) % mod;
for (int j = 1; j <= n; j++) {
if (i == j) continue;
for (int k = i + 1; k <= n + 1; k++)
mat[j][k] = (mat[j][k] - mat[j][i] * mat[i][k] % mod + mod) % mod;
mat[j][i] = 0;
}
}
return true;
}
signed main() {
// freopen("in.txt", "r", stdin);
// freopen("out.txt", "w", stdout);
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n + 1; j++) scanf("%lld", &mat[i][j]);
if (gauss(n))
for (int i = 1; i <= n; i++) printf("%lld\n", mat[i][n + 1]);
else
printf("No Solution\n");
return 0;
}
求解线性方程组( float 型)
#include <cmath>
#include <iostream>
using namespace std;
const int maxn = 111;
const double eps = 1e-8;
double mat[maxn][maxn];
bool gauss(int n) {
for (int i = 1; i <= n; i++) {
int pre = i;
for (int j = i + 1; j <= n; j++)
if (mat[j][i] > mat[pre][i]) pre = j;
if (pre != i) swap(mat[i], mat[pre]);
if (fabs(mat[i][i]) <= eps) return false;
for (int j = n + 1; j >= i; j--) mat[i][j] /= mat[i][i]; // 循环顺序不可改
for (int j = 1; j <= n; j++) {
if (i == j) continue;
for (int k = i + 1; k <= n + 1; k++)
mat[j][k] = (mat[j][k] - mat[j][i] * mat[i][k]);
mat[j][i] = 0;
}
}
return true;
}
int main() {
// freopen("in.txt", "r", stdin);
// freopen("out.txt", "w", stdout);
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n + 1; j++) scanf("%lf", &mat[i][j]);
if (gauss(n))
for (int i = 1; i <= n; i++) printf("%.2lf\n", mat[i][n + 1]);
else
printf("No Solution\n");
return 0;
}
计算行列式( int 型)
#include <iostream>
#define int long long
using namespace std;
const int maxn = 666;
int mod;
int mat[maxn][maxn];
int gauss(int n) {
int sign = 1, ans = 1;
for (int i = 1; i <= n; i++) {
int pre = i;
for (int j = i + 1; j <= n; j++)
if (mat[j][i] > mat[pre][i]) pre = j;
if (pre != i) swap(mat[i], mat[pre]), sign = -sign;
if (!mat[i][i]) return 0;
for (int j = i + 1; j <= n; j++) { // 上三角化
if (mat[j][i] > mat[i][i]) swap(mat[i], mat[j]), sign = -sign; // 会卡常
while (mat[j][i]) { // 辗转相除,最后得0
int tmp = mat[i][i] / mat[j][i];
for (int k = i; k <= n; k++)
mat[i][k] = (mat[i][k] - mat[j][k] * tmp % mod + mod) % mod;
swap(mat[i], mat[j]), sign = -sign;
}
}
ans = ans * mat[i][i] % mod;
}
ans = (mod + sign * ans) % mod;
return ans;
}
signed main() {
// freopen("in.txt", "r", stdin);
// freopen("out.txt", "w", stdout);
int n;
scanf("%d%d", &n, &mod);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) scanf("%lld", &mat[i][j]);
printf("%lld\n", gauss(n));
return 0;
}
计算行列式( float 型)
#include <cmath>
#include <iostream>
using namespace std;
const int maxn = 666;
const double eps = 1e-8;
double mat[maxn][maxn];
double gauss(int n) {
double ans = 1.0;
for (int i = 1; i <= n; i++) {
int pre = i;
for (int j = i + 1; j <= n; j++)
if (mat[j][i] > mat[pre][i]) pre = j;
if (pre != i) swap(mat[i], mat[pre]), ans = -ans;
if (fabs(mat[i][i]) <= eps) return 0;
ans *= mat[i][i];
for (int j = n; j >= i; j--) mat[i][j] /= mat[i][i];
for (int j = 1; j <= n; j++) {
if (i == j) continue;
for (int k = i + 1; k <= n; k++)
mat[j][k] = (mat[j][k] - mat[j][i] * mat[i][k]);
mat[j][i] = 0;
}
}
return ans;
}
int main() {
// freopen("in.txt", "r", stdin);
// freopen("out.txt", "w", stdout);
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) scanf("%lf", &mat[i][j]);
printf("%.2lf", gauss(n));
return 0;
}
矩阵求逆( int 型)
#include <iostream>
#define int long long
using namespace std;
const int maxn = 500;
const int mod = 1e9 + 7;
int mat[maxn][2 * maxn];
int quickpow(int a, int b) {
int ans = 1;
while (b) {
if (b & 1) ans = ans * a % mod;
a = a * a % mod, b >>= 1;
}
return ans;
}
bool gauss(int n) {
for (int i = 1; i <= n; i++) mat[i][n + i] = 1;
for (int i = 1; i <= n; i++) {
int pre = i;
for (int j = i + 1; j <= n; j++)
if (mat[j][i] > mat[pre][i]) pre = j;
if (pre != i) swap(mat[i], mat[pre]);
if (!mat[i][i]) return false;
for (int j = 2 * n; j >= i; j--)
mat[i][j] = mat[i][j] * quickpow(mat[i][i], mod - 2) % mod;
for (int j = 1; j <= n; j++) {
if (i == j) continue;
for (int k = i + 1; k <= 2 * n; k++)
mat[j][k] = (mat[j][k] - mat[j][i] * mat[i][k] % mod + mod) % mod;
mat[j][i] = 0;
}
}
return true;
}
signed main() {
// freopen("in.txt", "r", stdin);
// freopen("out.txt", "w", stdout);
int n;
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j) scanf("%lld", &mat[i][j]);
if (gauss(n)) {
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) printf("%lld ", mat[i][n + j]);
printf("\n");
}
} else
printf("No Solution");
return 0;
}
矩阵求逆( float 型)
#include <cmath>
#include <iostream>
using namespace std;
const int maxn = 500;
const double eps = 1e-8;
double mat[maxn][2 * maxn];
bool gauss(int n) {
for (int i = 1; i <= n; i++) mat[i][n + i] = 1;
for (int i = 1; i <= n; i++) {
int pre = i;
for (int j = i + 1; j <= n; j++)
if (mat[j][i] > mat[pre][i]) pre = j;
if (pre != i) swap(mat[i], mat[pre]);
if (fabs(mat[i][i]) <= eps) return false;
for (int j = 2 * n; j >= i; j--) mat[i][j] /= mat[i][i];
for (int j = 1; j <= n; j++) {
if (i == j) continue;
for (int k = i + 1; k <= 2 * n; k++)
mat[j][k] = (mat[j][k] - mat[j][i] * mat[i][k]);
mat[j][i] = 0;
}
}
return true;
}
int main() {
// freopen("in.txt", "r", stdin);
// freopen("out.txt", "w", stdout);
int n;
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j) scanf("%lf", &mat[i][j]);
if (gauss(n)) {
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) printf("%.2lf ", mat[i][n + j]);
printf("\n");
}
} else
printf("No Solution");
return 0;
}
二、线性基
我们先来个感性认识:
[ 5 12 2 7 9 ] ⟹ \begin{bmatrix} 5 \\ 12 \\ 2 \\ 7 \\ 9 \end{bmatrix} \implies ⎣ ⎡512279⎦ ⎤⟹ [ 0 1 0 1 1 1 0 0 0 0 1 0 0 1 1 1 1 0 0 1 ] ⟹ \begin{bmatrix} 0 & 1 & 0 & 1\\ 1 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 1 & 1 & 1 \\ 1 & 0 & 0 & 1 \end{bmatrix} \implies ⎣ ⎡01001110100011010011⎦ ⎤⟹ [ 1 0 0 1 0 1 0 1 0 0 1 0 0 0 0 0 0 0 0 0 ] \begin{bmatrix} 1 & 0 & 0 & 1\\ 0 & 1 & 0 & 1 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 \end{bmatrix} ⎣ ⎡10000010000010011000⎦ ⎤
这里的线性基和线代里的线性基还是有一些区别的,这里是在模 2 意义下的线性,
异或可以理解为模 2 下不进位加法,所以利用线性基可以处理很多异或相关问题
甚至看到异或就可以联想线性基
它的具体原理为:
先将数组展开为 n × 64 n\times 64 n×64 的矩阵,然后利用高斯消元,化简为最简阶梯型矩阵
这个阶梯型矩阵便是我们所说的线性基,利用它,我们可以解决:
-
最大、最小异或和
-
查询第 k 大异或和
-
查询某异或和排名
-
求所有异或值的和
-
查询某数是否可以被线性表出
-
线性基合并
-
求两个线性基的交
-
求两个线性基的并
实际上,线性基也可以和线段树套在一起,进而解决跟区间相关的问题
1.基础概念
下面我们来利用严谨的数学的数学工具来研究它:
异或和:
设 S S S 为一无符号整数集,定义其异或和 x o r ( S ) = ⊕ i = 1 ∣ S ∣ x i xor(S)=\oplus_{i=1}^{|S|}x_i xor(S)=⊕i=1∣S∣xi ,即其所含元素的一起异或的结果
张成:
定义 s p a n ( S ) = { x ∣ x = x o r ( T ) , T ⊆ S } span(S)=\{x|x=xor(T),T\sube S\} span(S)={x∣x=xor(T),T⊆S} 为集合 S S S 的张成
线性相关:
对于一个集合,
若其一个元素 x i x_i xi 可由集合内其他元素异或表出,则称集合线性相关
若不存这样的元素 x i x_i xi ,则称集合线性无关
线性相关: ∃ i , x i ∈ s p a n ( S / x i ) \exist i, x_i \in span(S/x_i) ∃i,xi∈span(S/xi)
线性无关: ∀ i , x i ∉ s p a n ( S / x i ) \forall i,x_i \notin span(S/x_i) ∀i,xi∈/span(S/xi)
线性基:
我们称 B a s e Base Base 为集合 S S S 的线性基,当且仅当:
s p a n ( S ) = s p a n ( B a s e ) span(S) = span(Base) span(S)=span(Base)
∀ i , x i ∉ s p a n ( B a s e / x i ) \forall i,x_i \notin span(Base/x_i) ∀i,xi∈/span(Base/xi)
那么它有如下性质:
-
线性基的元素能相互异或得到原集合的元素的所有相互异或得到的值。
-
线性基是满足性质 1 的最小的集合。
-
线性基没有异或和为 0 的子集。
-
线性基中每个元素的异或方案唯一,
也就是说,线性基中不同的异或组合异或出的数都是不一样的。
-
线性基中每个元素的二进制最高位互不相同。
2. 线性基构建
我们可以发现对于 S S S 的任一极大线性无关子集,都是 S S S 的一组线性基,这也说明 ∣ S ∣ ≥ ∣ B a s e ∣ |S| \ge |Base| ∣S∣≥∣Base∣
提供一种简单的在线构造方案为:
遍历 S S S 的每个元素 x i x_i xi ,将其转为二进制,从高位向低位扫,若第 j j j 位是 1 ,并且 a j a_j aj 不存在,那么令 a j = x i a_j=x_i aj=xi 并结束扫描,如果 a j a_j aj 存在,令 x i = x i ⊕ a j x_i = x_i \oplus a_j xi=xi⊕aj
来一道例题 P3812 【模板】线性基 - 洛谷
由于线性基的最高位互不相同,所以我们贪心地从最高位遍历到最低位即可
代码如下:
#include <iostream>
#define int long long
using namespace std;
int base[70];
void insert(int x) {
for (int i = 62; i >= 0; i--) {
if (!(x & (1ll << i))) continue; // x的第i位是0
if (!base[i]) { // 对角线上第一个 0 的位置
base[i] = x;
return;
}
x ^= base[i];
}
}
signed main() {
// freopen("in.txt", "r", stdin);
// freopen("out.txt", "w", stdout);
int n, x, res = 0;
scanf("%d", &n);
for (int i = 0; i < n; i++) scanf("%lld", &x), insert(x);
for (int i = 62; i >= 0; i--)
if ((res ^ base[i]) > res) res ^= base[i];
printf("%lld\n", res);
return 0;
}
3.线性基重构
但不是所有线性基都很好用,比如当我们查询第 k k k 小的异或和时,上面的方法就不太好了,回到我们最开头用高斯消元构造的线性基:
通过高斯消元重构,我们可以获得这种最简阶梯型线性基,进而可以解决更多种问题,
在阶梯形矩阵中,若非零行的第一个非零元素全是 1 ,且非零行的第一个元素 1 所在列的其余元素全为零,就称该矩阵为行最简形矩阵
那么比如我们要查询第 k k k 小的数,例如 k = ( 1100 ) 2 k=(1100)_2 k=(1100)2 那么答案为 b a s e 2 , b a s e 3 base_2,base_3 base2,base3 的异或和 ,重构后 b a s e i ∼ 2 i base_i \sim 2^i basei∼2i
给出参考代码,还是洛谷的 P3812模板题 :
#include <iostream>
#define int long long
using namespace std;
int base[70];
int cnt, zero;
void insert(int x) {
for (int i = 62; i >= 0; i--) {
if (!(x & (1ll << i))) continue; // x的第i位是0
if (!base[i]) { // 对角线上第一个 0 的位置
base[i] = x;
return;
}
x ^= base[i];
}
if (!x) zero = true;
}
void rebuild() {
for (int i = 62; i >= 0; i--)
for (int j = 62; j > i && base[i]; j--)
if (base[j] & (1ll << i)) base[j] ^= base[i];
for (int i = 0; i <= 62; i++)
if (base[i]) base[cnt++] = base[i];
}
int querykth(int k) {
if (k > (1ll << cnt)) return -1;
int ans = 0;
k -= zero;
for (int i = cnt; i >= 0; i--)
if (k & (1ll << i)) ans ^= base[i];
return ans;
}
signed main() {
// freopen("in.txt", "r", stdin);
// freopen("out.txt", "w", stdout);
int n, x, res = 0;
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%lld", &x), insert(x);
rebuild();
for (int i = 0; i < cnt; i++) res ^= base[i];
printf("%lld\n", res);
return 0;
}
再给出查询排名的模板:
#include <iostream>
#define int long long
using namespace std;
int base[70];
int cnt, zero;
void insert(int x) {
for (int i = 62; i >= 0; i--) {
if (!(x & (1ll << i))) continue; // x的第i位是0
if (!base[i]) { // 对角线上第一个 0 的位置
base[i] = x;
return;
}
x ^= base[i];
}
if (!x) zero = true;
}
void rebuild() {
for (int i = 62; i >= 0; i--)
for (int j = 62; j > i && base[i]; j--)
if (base[j] & (1ll << i)) base[j] ^= base[i];
for (int i = 0; i <= 62; i++)
if (base[i]) base[cnt++] = base[i];
}
int queryrank(int x) {
int ans = 0;
for (int i = 0; i < cnt; i++)
if (x >= base[i]) ans += (1ll << i), x ^= base[i];
return ans + zero;
}
signed main() {
// freopen("in.txt", "r", stdin);
// freopen("out.txt", "w", stdout);
int n, x;
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%lld", &x), insert(x);
rebuild();
printf("%lld\n", queryrank(0));
return 0;
}
note:
任意一条 1 到 n 的路径的异或和,都可以由任意一条 1 到 n 路径的异或和与图中的一些环的异或和来组合得到
最后给出一些习题:
三、一些常用模型
1. 范德蒙德行列式
范德蒙德行列式:
V n = ∣ 1 1 ⋯ 1 x 1 x 2 ⋯ x n ⋮ ⋮ ⋮ x 1 n − 1 x 2 n − 1 ⋯ x n n − 1 ∣ V_n=\begin{vmatrix} 1 & 1 & \cdots & 1 \\ x_1 & x_2 & \cdots & x_n \\ \vdots & \vdots & & \vdots \\ x_1^{n-1} & x_2^{n-1} & \cdots & x_n^{n-1} \end{vmatrix} Vn=∣ ∣1x1⋮x1n−11x2⋮x2n−1⋯⋯⋯1xn⋮xnn−1∣ ∣
对于这个行列式,它有如下性质:
V n = ∏ 1 ≤ i < j ≤ n ( x j − x i ) \displaystyle V_n=\prod_{1\le i<j\le n}(x_j-x_i) Vn=1≤i<j≤n∏(xj−xi) ,可由归纳法证出
2. 上海森堡矩阵
上海森堡矩阵是用来求矩阵特征值的有力工具,与之配套的还有 H e s s e n b e r g Hessenberg Hessenberg 算法
具体证明详见 特征多项式 - OI Wiki
3. 有用的网址
-
线性代数全家桶-ACM_繁凡さん的博客 里面有很全的线代知识,有本文未涉及的线性基套线段树和线性基的交并
-
《线性代数》概念定理大全!_繁凡さん的博客 里面内容偏向于数学,讲述了具体的数学知识
-
数学之美 - 随笔分类 - seniusen - 博客园 里面也是比较纯粹的数学知识,不过涉及到了更高级的内容
-
线性基学习笔记 | Sengxian’s Blog 里面给出了一些关于线性基的例题分析