跳转至

高斯消元

高斯消元用于在 \(O(n^3)\) 时间内实现各种矩阵相关的计算,如矩阵行列式矩阵求逆解线性方程组等。

高斯消元的核心是:将原矩阵通过初等行变换,转换为行最简形。

行最简形

行阶梯形矩阵满足:

  • 非零行的首非零元为 \(1\)
  • 首非零元所在的列的其他元均为 \(0\)

则称此矩阵为行最简形矩阵。例如:

\[ A= \left( \begin{matrix} 1& 0& -1& 0& 4\\ 0& 1& -1& 0& 3\\ 0& 0& 0& 1& -3\\ 0& 0& 0& 0& 0\\ \end{matrix} \right) \]

对于给矩阵 \(A\),我们从左到右考虑 \(A\) 的每一列,并将前 \(i\) 列消元的结果存储在前 \(cur\) 行。显然有 \(cur\le i\),当且仅当前 \(i\) 列满秩,等号成立。

  • 如果第 \(i\) 列全都为 \(0\),则 \(cur\) 不变,直接跳过该列;
  • 否则:
  • 选取 \([cur,n]\) 行中该列绝对值最大的一行,将它交换到第 \(cur\) 行(主元法,为了减小误差);
  • 将第 \(cur\) 行除以 \(a_{cur,i}\),使 \(a_{cur,i}\) 变为 \(1\)
  • 用第 \(cur\) 行消去其它行的第 \(i\) 列;
  • \(cur\leftarrow cur+1\)

经过这些消元之后,矩阵将会变为行最简形。当矩阵满秩时,其左侧是一个单位矩阵。若矩阵不满秩,则所有空行将堆积在矩阵的下部。

关于主元法

如果主元 \(a_{cur,i}\) 的绝对值太小,后续消元步骤中需要将其他元素除以该主元,导致乘数极大,这种放大效应会使原有数据中的微小舍入误差被显著放大。主元法通过选择较大的主元,确保除数不会过小,从而控制乘数的量级。

由 deepseek 回答

int cur = 1;
for(int i = 1; i <= n; i++) {
    int mx = cur;
    for(int j = cur + 1; j <= n; j++) {
        if(abs(a[j][i]) > abs(a[mx][i])) mx = j;
    }
    if(abs(a[mx][i]) < eps) continue;
    for(int j = 1; j <= n + 1; j++) {
        swap(a[cur][j], a[mx][j]);
    }
    for(int j = n + 1; j >= i; j--) {
        a[cur][j] /= a[cur][i];
    }
    for(int j = 1; j <= n; j++) {
        if(j == cur) continue;
        for(int k = n + 1; k >= i; k--) {
            a[j][k] -= a[j][i] * a[cur][k];
        }
    }
    ++cur;
}

求解线性方程组

P2455 [SDOI2006] 线性方程组

题意

已知 \(n\) 元线性一次方程组。

\[ \begin{cases} a_{1, 1} x_1 + a_{1, 2} x_2 + \cdots + a_{1, n} x_n = b_1 \\ a_{2, 1} x_1 + a_{2, 2} x_2 + \cdots + a_{2, n} x_n = b_2 \\ \cdots \\ a_{n,1} x_1 + a_{n, 2} x_2 + \cdots + a_{n, n} x_n = b_n \end{cases}\]

请根据输入的数据,编程输出方程组的解的情况。

如果有唯一解,则输出解。你的结果被认为正确,当且仅当对于每一个 \(x_i\) 而言结果值与标准答案值的绝对误差不超过 \(0.01\)

如果方程组无解输出 \(-1\); 如果有无穷多实数解,输出 \(0\)

对于 \(100\%\) 的数据,\(1 \le n \le 50\)。对于 \(\forall 1\le i, j \le n\),有 \(|a_{i, j}| \le 100\)\(|b_i| \le 300\)

将所有未知数的系数排成一个矩阵,然后将常数项放在右边,得到的矩阵叫做增广矩阵

\[ \left( \begin{matrix} a_{1,1}& a_{1,2}& a_{1,3}& \cdots& a_{1,m}\\ a_{2,1}& a_{2,2}& a_{2,3}& \cdots& a_{2,m}\\ a_{3,1}& a_{3,2}& a_{3,3}& \cdots& a_{3,m}\\ & \vdots & & \ddots& \vdots\\ a_{n,1}& a_{n,2}& a_{n,3}& \cdots& a_{n,m}\\ \end{matrix} \ \middle|\ \begin{matrix} b_1\\ b_2\\ b_3\\ \vdots\\ b_n \end{matrix} \right) \]

我们希望增广矩阵的左侧变为一个单位矩阵,这样右侧的常数项就是方程的解:

\[ \left( \begin{matrix} 1& 0& 0& \cdots& 0& 0\\ 0& 1& 0& \cdots& 0& 0\\ 0& 0& 1& \cdots& 0& 0\\ & \vdots & & \ddots& \vdots& \vdots\\ 0& 0& 0& \cdots& 1& 0\\ 0& 0& 0& \cdots& 0& 1\\ \end{matrix} \ \middle|\ \begin{matrix} x_1\\ x_2\\ x_3\\ \vdots\\ x_{n-1}\\ x_n \end{matrix} \right) \]

我们只需要对原始的增广矩阵进行高斯消元,使其左半部分变为行最简形即可。

对于无穷解的情况,矩阵下半部分会有空行;对于无解的情况,将会存在一行,系数项全都为 \(0\),而常数项不为 \(0\)\(0\ne 0\))。特判这两种情况即可。

代码
#include<iostream>
#include<iomanip>
#include<cstdio>
#include<cmath>
#define ld long double
using namespace std;
const int N = 55;
const ld eps = 1e-6l;

int n;
ld a[N][N];

int main() {

    cin >> n;
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= n + 1; j++) {
            cin >> a[i][j];
        }
    }

    int cur = 1;
    for(int i = 1; i <= n; i++) {
        int mx = cur;
        for(int j = cur + 1; j <= n; j++) {
            if(abs(a[j][i]) > abs(a[mx][i])) mx = j;
        }
        if(abs(a[mx][i]) < eps) continue;
        for(int j = 1; j <= n + 1; j++) {
            swap(a[cur][j], a[mx][j]);
        }
        for(int j = n + 1; j >= i; j--) {
            a[cur][j] /= a[cur][i];
        }
        for(int j = 1; j <= n; j++) {
            if(j == cur) continue;
            for(int k = n + 1; k >= i; k--) {
                a[j][k] -= a[j][i] * a[cur][k];
            }
        }
        ++cur;
    }

    for(int i = 1; i <= n; i++) {
        if(abs(a[i][n + 1]) < eps) continue;
        bool flag = true;
        for(int j = i; j <= n; j++) {
            if(abs(a[i][j]) > eps) {
                flag = false;
                break;
            } 
        }
        if(flag) {
            cout << -1 << endl;
            return 0;
        }
    }

    for(int i = 1; i <= n; i++) {
        if(abs(a[i][i]) < eps) {
            cout << 0 << endl;
            return 0;
        }
    }

    for(int i = 1; i <= n; i++) {
        printf("x%d=%.2Lf\n", i, a[i][n + 1]);
    }

    return 0;
}

/*
4
0 0 1 1 1
0 0 1 1 2
0 0 0 0 0
0 0 0 0 0

10
1 1 2 5 4 3 4 6 2 1  1
5 4 8 1 3 6 4 1 4 3  2
6 4 2 5 1 3 1 4 2 6  3
1 9 5 6 7 3 2 8 4 2  4
2 9 4 8 6 7 5 3 1 5  5
5 6 8 4 7 1 6 3 4 2  6
8 1 4 7 6 5 2 6 9 3  7
9 4 7 5 6 3 2 1 4 7  8
6 4 8 5 7 3 2 1 9 4  9
10 3 8 17 14 11 10 18 13 5 9

10
1 1 2 5 4 3 4 0 2 1  1
5 4 8 1 3 6 4 0 4 3  2
6 4 2 5 1 3 1 0 2 6  3
1 9 5 6 7 3 2 0 4 2  4
2 9 4 8 6 7 5 0 1 5  5
5 6 8 4 7 1 6 0 4 2  6
8 1 4 7 6 5 2 0 9 3  7
9 4 7 5 6 3 2 0 4 7  8
6 4 8 5 7 3 2 0 9 4  9
9 3 6 3 9 0 1 0 5 1  10

2  2 4 10 8  6  8  12 4  2 2
8  1 4 7  6  5  2   6 9  3 7
10 3 8 17 14 11 10 18 13 5 9
*/

Y1-4 图上游走

在一些题目中,各个变量的关系较为复杂,没有简单的递推式。我们就可以使用高斯消元,解出答案。

概率与期望

求解行列式 & 数论高消

P7112 【模板】行列式求值

题意

给定一个 \(n\) 阶行列式 \(A\) 和模数 \(p\),求 \(|A|\bmod p\)。不保证 \(p\) 是质数。

我们只需要把 \(A\) 消成上三角矩阵即可,考虑如何在没有逆元的情况下求出精确解。我们可以使用欧几里德算法,使用模运算代替有理数除法。我们依次枚举每一列 \(i\),然后尝试把该列的 \([i+1,n]\) 行都消为 \(0\)。对于每一个 \(k\in [i+1,n]\),我们只需要将 \(a_{i,i}\)\(a_{k,i}\) 进行辗转相除(所在的整行也同时进行运算),一定能把其中一项消成 \(0\),然后把第 \(i\) 列非零的那一行交换到第 \(i\) 行即可。最终得到一个上三角矩阵。

容易发现时间消耗不超过 \(O(n^3\log V)\)。然而我们可以进一步证明时间复杂度为 \(O(n^3)\)

时间复杂度分析

能够证明,使用欧几里德算法求出 \(n\) 个数的 \(\gcd\),均摊进行 \(O(n+\log V)\) 次取模操作(证明详见欧几里德算法)。

每对 \(a_{i,i}\)\(a_{k,i}\) 进行一次取模操作,都会对 \(i,k\) 两行进行变换,产生 \(O(n)\) 的时间复杂度。因此总时间为 \(O(n^2\log V+n^3)\)

数论高消的核心是:用取模运算(本质是加减)代替有理数除法,可以在相同的时间复杂度内将原矩阵消为上三角矩阵。

代码
#include<iostream>
#define int long long
using namespace std;
const int N = 610;

int n, ans = 1, MOD;
int a[N][N];

int op[2][2];

void md(int &x) {
    x = (x % MOD + MOD) % MOD;
}

signed main() {

    cin >> n >> MOD;
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= n; j++) {
            cin >> a[i][j];
        }
    }

    for(int i = 1; i <= n; i++) {
        for(int j = i + 1; j <= n; j++) {
            if(a[i][i] < a[j][i]) {
                for(int k = n; k >= i; k--) {
                    swap(a[i][k], a[j][k]);
                }
                ans = MOD - ans;
            }
            if(!a[i][i]) continue;
            while(a[j][i]) {
                for(int k = n; k >= i + 1; k--) {
                    int x = a[i][k], y = a[j][k];
                    a[i][k] = y;
                    a[j][k] = x - (a[i][i] / a[j][i]) * y % MOD;
                    md(a[j][k]);
                }
                a[i][i] %= a[j][i];
                swap(a[i][i], a[j][i]);
                ans = MOD - ans;
            }
        }
    }

    for(int i = 1; i <= n; i++) ans = ans * a[i][i] % MOD;

    cout << ans << endl;

    return 0;
}