算法竞赛中的线性代数

本文深入探讨了线性代数中的高斯-约旦消元法,包括求解线性方程组、计算行列式和矩阵求逆。详细阐述了算法原理和实现,并对比了整数和浮点数类型的处理差异。此外,介绍了线性基的概念,包括基础理论、构建与重构方法,以及线性基在异或问题中的应用。文章还列举了相关练习题和参考资料,帮助读者深入理解这些线性代数工具。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线性代数

一、高斯-约旦消元

1. 求解线性方程组

约旦消元法的精度更好、代码更简单,没有回带的过程

  1. 选择一个尚未被选过的未知数作为主元,选择一个包含这个主元的方程

  2. 这个方程主元的系数化为1

  3. 通过加减消元,消掉其它所有方程中的这个未知数

  4. 重复以上步骤,直到把每一行都变成只有一项有系数

给出一道例题:P3389 【模板】高斯消元法 - 洛谷

算法说明:

高斯约旦消元是一列一列来处理的,

对于第 i i i 列,其前 i − 1 i-1 i1 列除了对角线上的元为 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 1n 行用第 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 i1 列已变成上三角阵了

利用辗转相除法,对第 i ∼ n i \sim n in 行进行辗转相除,使得 i + 1 ∼ n i+1\sim n i+1n 行的 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,A1]

给出一道例题: 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 的矩阵,然后利用高斯消元,化简为最简阶梯型矩阵

这个阶梯型矩阵便是我们所说的线性基,利用它,我们可以解决:

  1. 最大、最小异或和

  2. 查询第 k 大异或和

  3. 查询某异或和排名

  4. 求所有异或值的和

  5. 查询某数是否可以被线性表出

  6. 线性基合并

  7. 求两个线性基的交

  8. 求两个线性基的并

实际上,线性基也可以和线段树套在一起,进而解决跟区间相关的问题

1.基础概念

下面我们来利用严谨的数学的数学工具来研究它:

异或和:

S S S 为一无符号整数集,定义其异或和 x o r ( S ) = ⊕ i = 1 ∣ S ∣ x i xor(S)=\oplus_{i=1}^{|S|}x_i xor(S)=i=1Sxi ,即其所含元素的一起异或的结果

张成:

定义 s p a n ( S ) = { x ∣ x = x o r ( T ) , T ⊆ S } span(S)=\{x|x=xor(T),T\sube S\} span(S)={xx=xor(T),TS} 为集合 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,xispan(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| SBase

提供一种简单的在线构造方案为:

遍历 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=xiaj

来一道例题 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 basei2i

给出参考代码,还是洛谷的 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= 1x1x1n11x2x2n11xnxnn1

对于这个行列式,它有如下性质:

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=1i<jn(xjxi) ,可由归纳法证出

2. 上海森堡矩阵

上海森堡矩阵是用来求矩阵特征值的有力工具,与之配套的还有 H e s s e n b e r g Hessenberg Hessenberg 算法

具体证明详见 特征多项式 - OI Wiki

3. 有用的网址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值