Chương 6·Bài 63

Gaussian Elimination

Giải hệ x+y=5x + y = 5, 2x−y=12x - y = 1 ai cũng biết: cộng hai phương trình để mất yy, ra 3x=63x = 6. Với 1000 ẩn thì cách làm đó vẫn dùng được, chỉ cần làm có trật tự.

Khử Gauss dùng ba phép biến đổi hàng: đổi chỗ hai hàng, nhân một hàng với số khác 0, cộng bội của một hàng vào hàng khác. Chúng đưa ma trận về dạng bậc thang trong O(n3)O(n^3), rồi đọc ra hệ vô nghiệm, vô số nghiệm hay nghiệm duy nhất. Cùng một khung chạy trên số thực (cần hàng trụ tốt và ngưỡng sai số), trên mô-đun nguyên tố (chính xác), trên GF(2) (một hàng là một bitset), và tính được định thức, ma trận nghịch đảo, kỳ vọng trên đồ thị.

Gauss-Jordan

Ma trận mở rộng [A∣b][A \mid b] cỡ n×(n+1)n \times (n + 1). Duyệt từng cột cc với con trỏ hàng rr:

  1. Trong các hàng r..n−1r..n-1, chọn hàng có ∣aic∣|a_{ic}| lớn nhất làm hàng trụ. Nếu nó nhỏ hơn ε\varepsilon, cột cc không có trụ (biến tự do), sang cột sau.
  2. Đổi hàng trụ lên vị trí rr, rồi khử cột cc ở mọi hàng khác (Gauss-Jordan), tăng rr.

Sau cùng: nếu một hàng i≥ri \ge r có vế phải khác 0 thì hệ vô nghiệm (hàng 0=bi0 = b_i); ngược lại nếu r<nr < n thì vô số nghiệm; còn lại nghiệm duy nhất xi=bi/aiix_i = b_i / a_{ii}. Kiểm vô nghiệm trước, vì hệ có thể vừa có biến tự do vừa mâu thuẫn.

Chọn trụ lớn nhất giữ sai số nhỏ, vì chia cho số gần 0 khuếch đại sai số làm tròn. Ngưỡng ε\varepsilon khoảng 10−810^{-8} với double, nhỏ hơn với long double. Khi in, đổi giá trị có trị tuyệt đối dưới nửa đơn vị cuối thành 0 để khỏi in -0.00.

Trong hình, ô cam là phần tử trụ, hàng xanh là hàng trụ. Sau mỗi lần khử, cột trụ chỉ còn một ô khác 0.

Từng bước Gauss-Jordan.

Hỏi nhanh

Sau khi khử, hệ ba ẩn còn hàng 0⋅x1+0⋅x2+0⋅x3=50 \cdot x_1 + 0 \cdot x_2 + 0 \cdot x_3 = 5, và chỉ có hai cột có trụ. Kết luận nào đúng?

Ví dụBa Trạng Thái Của Hệ

Đề. n≤50n \le 50 phương trình nn ẩn, hệ số nguyên nhỏ. In −1-1 nếu vô nghiệm, 00 nếu vô số nghiệm, ngược lại in từng xi= giá trị với hai chữ số thập phân.

Lời giải. Khử Gauss-Jordan trên ma trận mở rộng n×(n+1)n \times (n + 1). Cột không có trụ (mọi phần tử gần 0) là một ẩn tự do. Sau khi khử, một hàng toàn 0 bên trái mà vế phải khác 0 nghĩa là vô nghiệm; không vô nghiệm mà có ẩn tự do thì vô số nghiệm; còn lại nghiệm duy nhất. Tính bằng long double với ε=10−11\varepsilon = 10^{-11}, để giá trị đúng bằng nửa đơn vị làm tròn (như 0,1250{,}125) không bị sai số đẩy sang phía kia.

Bước 1: chọn trụ lớn nhất. r là số hàng trụ đã dùng. Với mỗi cột c, tìm hàng có |a[p][c]| lớn nhất từ hàng r trở đi. Trụ lớn nhất làm hệ số f không vượt 1 về trị tuyệt đối, sai số không bị khuếch đại. Trụ gần 0 thì bỏ qua cột, r không tăng.

C++
int p = r;
for (int i = r; i < n; i++)
    if (fabsl(a[i][c]) > fabsl(a[p][c])) p = i;
if (fabsl(a[p][c]) < EPS) continue;
swap(a[p], a[r]);

Bước 2: khử mọi hàng khác. Khử cột c ở mọi hàng khác, cả phía trên: đây là Gauss-Jordan, xong là đọc được nghiệm luôn.

C++
for (int i = 0; i < n; i++)
    if (i != r) {
        ld f = a[i][c] / a[r][c];
        for (int j = c; j <= n; j++) a[i][j] -= f * a[r][j];
    }
r++;

Bước 3: phân loại. Các hàng từ r trở đi có vế trái toàn 0; vế phải khác 0 ở hàng nào là vô nghiệm. r < n mà không vô nghiệm là vô số nghiệm. r == n thì xc=acn/accx_c = a_{cn} / a_{cc}; số rất gần 0 đưa về 0 để khỏi in -0.00.

C++
for (int i = r; i < n; i++)
    if (fabsl(a[i][n]) > EPS) {
        cout << -1 << '\n';
        return 0;
    }
if (r < n) {
    cout << 0 << '\n';
    return 0;
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ld long double

const ld EPS = 1e-11;
int n;
ld a[55][55];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (int i = 0; i < n; i++)
        for (int j = 0; j <= n; j++) cin >> a[i][j];
    int r = 0;
    for (int c = 0; c < n && r < n; c++) {
        int p = r;
        for (int i = r; i < n; i++)
            if (fabsl(a[i][c]) > fabsl(a[p][c])) p = i;
        if (fabsl(a[p][c]) < EPS) continue;
        swap(a[p], a[r]);
        for (int i = 0; i < n; i++)
            if (i != r) {
                ld f = a[i][c] / a[r][c];
                for (int j = c; j <= n; j++) a[i][j] -= f * a[r][j];
            }
        r++;
    }
    for (int i = r; i < n; i++)
        if (fabsl(a[i][n]) > EPS) {
            cout << -1 << '\n';
            return 0;
        }
    if (r < n) {
        cout << 0 << '\n';
        return 0;
    }
    cout << fixed << setprecision(2);
    for (int i = 0; i < n; i++) {
        ld x = a[i][n] / a[i][i];
        if (fabsl(x) < 0.0005) x = 0;
        cout << "x" << i + 1 << "=" << (double)x << '\n';
    }
}
2
1 1 5
2 -1 1

Độ phức tạp: O(n3)O(n^3).

Ma trận nghịch đảo

Theo mô-đun nguyên tố pp, mọi số khác 0 có nghịch đảo, nên khử Gauss chạy chính xác: hàng trụ là hàng bất kỳ có phần tử khác 0, chia bằng nhân nghịch đảo (lũy thừa nhanh), không cần ε\varepsilon.

Ma trận nghịch đảo: khử Gauss-Jordan trên [A∣I][A \mid I]. Khi bên trái thành II, bên phải là A−1A^{-1}, vì dãy phép biến đổi hàng chính là nhân trái với A−1A^{-1}. Gặp cột không tìm được trụ thì AA không khả nghịch. Ở cột cc, hàng trụ đã bằng 0 ở các cột trước cc nên chỉ cần khử từ cột cc trở đi.

Ví dụMa Trận Đảo

Đề. Ma trận n×nn \times n, n≤400n \le 400, phần tử theo mô-đun 109+710^9 + 7. In A−1A^{-1} hoặc No Solution.

Lời giải. Ghép ma trận đơn vị vào bên phải thành [A∣I][A \mid I] cỡ n×2nn \times 2n, khử Gauss-Jordan theo mô-đun. Mỗi phép biến đổi hàng là nhân trái với một ma trận, nên khi bên trái thành II thì tích các phép đó là A−1A^{-1}, và bên phải chính là A−1A^{-1}. Theo mô-đun nguyên tố thì mọi phần tử khác 0 đều có nghịch đảo, nên trụ là bất kỳ phần tử khác 0 nào; không tìm được trụ ở một cột là AA suy biến.

Bước 1: ma trận mở rộng. a[i][n + i] = 1 đặt ma trận đơn vị bên phải.

Bước 2: tìm trụ. Hàng p từ c trở xuống có phần tử khác 0, đổi lên hàng c.

C++
int p = c;
while (p < n && !a[p][c]) p++;
if (p == n) {
    cout << "No Solution\n";
    return 0;
}
swap(a[p], a[c]);

Bước 3: chuẩn hóa và khử. Nhân cả hàng trụ với nghịch đảo của trụ để trụ thành 1, rồi trừ f lần hàng trụ khỏi mọi hàng khác để cột c chỉ còn số 1 ở hàng c. Vòng j chạy từ c vì các cột trước của hàng trụ đã bằng 0. Phép trừ cộng thêm MOD để kết quả trong [0,p)[0, p).

C++
ll inv = power(a[c][c], MOD - 2);
for (int j = c; j < 2 * n; j++) a[c][j] = a[c][j] * inv % MOD;
for (int i = 0; i < n; i++)
    if (i != c && a[i][c]) {
        ll f = a[i][c];
        for (int j = c; j < 2 * n; j++) a[i][j] = (a[i][j] - f * a[c][j] % MOD + MOD) % MOD;
    }
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll MOD = 1e9 + 7;
int n;
ll a[405][810];

ll power(ll x, ll k) {
    ll r = 1;
    x %= MOD;
    while (k) {
        if (k & 1) r = r * x % MOD;
        x = x * x % MOD;
        k >>= 1;
    }
    return r;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) cin >> a[i][j];
        a[i][n + i] = 1;
    }
    for (int c = 0; c < n; c++) {
        int p = c;
        while (p < n && !a[p][c]) p++;
        if (p == n) {
            cout << "No Solution\n";
            return 0;
        }
        swap(a[p], a[c]);
        ll inv = power(a[c][c], MOD - 2);
        for (int j = c; j < 2 * n; j++) a[c][j] = a[c][j] * inv % MOD;
        for (int i = 0; i < n; i++)
            if (i != c && a[i][c]) {
                ll f = a[i][c];
                for (int j = c; j < 2 * n; j++) a[i][j] = (a[i][j] - f * a[c][j] % MOD + MOD) % MOD;
            }
    }
    for (int i = 0; i < n; i++) {
        for (int j = n; j < 2 * n; j++) cout << a[i][j] << ' ';
        cout << '\n';
    }
}
2
1 2
3 5

Ước lượng phép tính: n⋅n⋅2n≈1,3⋅108n \cdot n \cdot 2n \approx 1{,}3 \cdot 10^8 phép nhân theo mô-đun.

GF(2) và bitset

Phương trình XOR (bật tắt đèn, kiểm chẵn lẻ) là hệ trên GF(2): cộng là XOR, nhân là AND, mọi hệ số khác 0 đều bằng 1. Một hàng gồm nn hệ số và vế phải là một bitset, nên khử một hàng tốn O(n/64)O(n / 64), cả hệ O(n3/64)O(n^3 / 64).

Khử Gauss trên GF(2), ma trận 1500 × 1500, trung vị 5 lần chạy, i7-10850H, g++ -O2.mảng bool từng ô370 ms · gấp 13 lầnbitset29 ms
Khử Gauss trên GF(2) với ma trận 1500 × 1500, hai cách lưu hàng, đo trên i7-10850H, g++ -O2.

bitset nhanh hơn mảng bool khoảng 13 lần: mỗi phép XOR xử lý 64 hệ số cùng lúc. Quy tắc: hệ mod 2 thì hàng luôn là bitset.

Khi phương trình đến lần lượt và cần biết lúc nào đủ để xác định nghiệm, chèn từng phương trình như chèn vào cơ sở XOR: duyệt biến từ nhỏ tới lớn, gặp biến đã có hàng trụ thì XOR với hàng đó, gặp biến chưa có thì nhận phương trình làm hàng trụ của biến ấy. Hạng đạt nn là lúc nghiệm duy nhất; sau đó khử ngược từ biến lớn về nhỏ để đọc nghiệm.

Ví dụMốc Đủ Dữ Kiện

Đề. N≤1000N \le 1000 bit chưa biết, M≤2000M \le 2000 phép đo theo thứ tự, mỗi phép đo cho XOR của một tập bit (dữ liệu nhất quán). Tìm KK nhỏ nhất để KK phép đo đầu xác định duy nhất mọi bit và in các bit (?y7M# cho 1, Earth cho 0), hoặc Cannot Determine.

Lời giải. Mỗi phép đo là một phương trình trên GF(2), lưu thành bitset gồm NN hệ số và vế phải ở bit NN. Chèn lần lượt như chèn vào cơ sở XOR; phép đo đầu tiên làm hạng đạt NN là KK. Sau đó khử ngược để mỗi hàng chỉ còn biến trụ của nó, và vế phải chính là giá trị của biến.

Bước 1: chèn một phép đo. row[j] là hàng trụ của biến jj: bit thấp nhất bật của nó là jj. Đi jj từ 0 lên: bit jj tắt thì bỏ qua; bật mà chưa có trụ thì nhận làm trụ và dừng; bật mà đã có trụ thì XOR để tắt bit jj, rồi đi tiếp. Phép đo đi hết mà không thành trụ là phụ thuộc các phép đo trước, không thêm thông tin. Dữ liệu nhất quán nên không cần kiểm mâu thuẫn.

C++
for (int j = 0; j < n; j++) {
    if (!v[j]) continue;
    if (!has[j]) {
        has[j] = 1, row[j] = v, rk++;
        break;
    }
    v ^= row[j];
}
if (rk == n) K = k;

Vòng đọc có điều kiện !K nên dừng ngay khi đủ hạng; các phép đo còn lại không cần đọc.

Bước 2: khử ngược. jj từ N−1N - 1 về 0. Lúc xử lý jj, các hàng t>jt > j đã chỉ còn bit tt, nên XOR với chúng xóa sạch các bit lớn hơn jj trong row[j]. Xong thì row[j][n] là giá trị của biến jj.

C++
for (int j = n - 1; j >= 0; j--)
    for (int t = j + 1; t < n; t++)
        if (row[j][t]) row[j] ^= row[t];
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N = 1005;
int n, m, rk;
bitset<N> row[N];
bool has[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> m;
    int K = 0;
    for (int k = 1; k <= m && !K; k++) {
        string s;
        int b;
        cin >> s >> b;
        bitset<N> v;
        for (int j = 0; j < n; j++) v[j] = s[j] == '1';
        v[n] = b;
        for (int j = 0; j < n; j++) {
            if (!v[j]) continue;
            if (!has[j]) {
                has[j] = 1, row[j] = v, rk++;
                break;
            }
            v ^= row[j];
        }
        if (rk == n) K = k;
    }
    if (!K) {
        cout << "Cannot Determine\n";
        return 0;
    }
    for (int j = n - 1; j >= 0; j--)
        for (int t = j + 1; t < n; t++)
            if (row[j][t]) row[j] ^= row[t];
    cout << K << '\n';
    for (int j = 0; j < n; j++) cout << (row[j][n] ? "?y7M#" : "Earth") << '\n';
}
3 4
110 1
011 1
101 0
001 1

Độ phức tạp: mỗi lần XOR hai bitset dài 1001 bit tốn khoảng 16 phép trên số 64 bit: tổng O(MN2/64)O(MN^2 / 64).

Định thức

Định thức là tích các phần tử trên đường chéo sau khi đưa về tam giác trên, mỗi lần đổi hai hàng thì đổi dấu. Theo mô-đun nguyên tố thì khử như thường.

Khi mô-đun không nguyên tố, phần tử trụ có thể không có nghịch đảo, nên khử kiểu Euclid: với hai hàng ii và jj, trừ ⌊aii/aji⌋\lfloor a_{ii} / a_{ji} \rfloor lần hàng jj khỏi hàng ii rồi đổi chỗ, lặp tới khi aji=0a_{ji} = 0. Các phép này chỉ trừ bội nguyên, không chia. Trong một cột, giá trị trụ chỉ giảm qua các lần gọi Euclid, nên tổng số bước của cột là O(n+log⁡p)O(n + \log p) và cả thuật toán O(n2(n+log⁡p))O(n^2 (n + \log p)).

Hỏi nhanh

Ma trận (1234)\begin{pmatrix}1 & 2 \\ 3 & 4\end{pmatrix} có định thức theo mô-đun 8 bằng bao nhiêu?

Ví dụĐịnh Thức Trên Vành

Đề. Ma trận n×nn \times n, n≤600n \le 600, phần tử dưới p≤109+7p \le 10^9 + 7, pp có thể là hợp số. In det⁡A mod p\det A \bmod p.

Lời giải. pp có thể là hợp số nên không chia được. Đưa ma trận về tam giác trên bằng cách làm như thuật toán Euclid: với cột ii và mỗi hàng j>ij > i, trừ ⌊aii/aji⌋\lfloor a_{ii} / a_{ji} \rfloor lần hàng jj khỏi hàng ii rồi đổi chỗ hai hàng, tới khi aji=0a_{ji} = 0. Trừ bội của một hàng không đổi định thức, đổi hai hàng thì đổi dấu. Cuối cùng định thức là tích đường chéo, nhân thêm dấu.

Bước 1: hàng là con trỏ. row[i] trỏ tới hàng ii; swap(row[i], row[j]) đổi hai hàng trong O(1)O(1) thay vì chép 600 số.

Bước 2: một bước Euclid. Trừ t lần hàng j khỏi hàng i (chỉ từ cột i, các cột trước đã bằng 0), rồi đổi chỗ và đổi dấu. Phép trừ có thể ra số âm nên cộng p. Giống Euclid, số bước là O(log⁡p)O(\log p).

C++
while (row[j][i]) {
    ll t = row[i][i] / row[j][i];
    if (t)
        for (ll k = i; k < n; k++) {
            row[i][k] = (row[i][k] - t * row[j][k]) % p;
            if (row[i][k] < 0) row[i][k] += p;
        }
    swap(row[i], row[j]);
    det = (p - det) % p;
}

Bước 3: tích đường chéo. det = 1 % p ở đầu để đúng cả khi p=1p = 1; sau mỗi cột nhân thêm row[i][i].

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

ll n, p, a[605][605], *row[605];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> p;
    for (ll i = 0; i < n; i++) {
        row[i] = a[i];
        for (ll j = 0; j < n; j++) cin >> a[i][j];
    }
    ll det = 1 % p;
    for (ll i = 0; i < n; i++) {
        for (ll j = i + 1; j < n; j++)
            while (row[j][i]) {
                ll t = row[i][i] / row[j][i];
                if (t)
                    for (ll k = i; k < n; k++) {
                        row[i][k] = (row[i][k] - t * row[j][k]) % p;
                        if (row[i][k] < 0) row[i][k] += p;
                    }
                swap(row[i], row[j]);
                det = (p - det) % p;
            }
        det = det * row[i][i] % p;
    }
    cout << det << '\n';
}
2 8
1 2
3 4

Độ phức tạp: O(n3+n2log⁡p)O(n^3 + n^2 \log p).

Lập hệ kỳ vọng

Bước đi ngẫu nhiên trên đồ thị có chu trình không DP theo thứ tự được, nhưng kỳ vọng số lần ghé mỗi đỉnh thỏa một hệ tuyến tính. Gọi xvx_v là kỳ vọng số lần quá trình đứng ở vv:

xv=[v=s]+∑u→vP(u→v)⋅xu.x_v = [v = s] + \sum_{u \to v} P(u \to v) \cdot x_u.

Hệ có nghiệm duy nhất khi quá trình chắc chắn dừng (có xác suất dừng dương). Xác suất dừng tại vv bằng xvx_v nhân xác suất dừng mỗi lần ghé. Với nn vài trăm, Gauss O(n3)O(n^3) là đủ.

Ví dụĐiểm Dừng Của Hành Trình

Đề. Đồ thị vô hướng N≤300N \le 300 đỉnh. Bắt đầu tại 1; ở mỗi đỉnh, với xác suất r=P/Qr = P/Q thì dừng, ngược lại đi theo một cạnh kề ngẫu nhiên. In xác suất dừng tại từng đỉnh.

Lời giải. Gọi xvx_v là kỳ vọng số lần đứng ở vv. Lần đứng ở vv hoặc là lúc bắt đầu (v=1v = 1), hoặc đến từ một hàng xóm uu: đứng ở uu, không dừng (xác suất 1−r1 - r), và chọn đúng cạnh tới vv (xác suất 1/deg⁡u1 / \deg u). Vậy

xv−∑u∼v1−rdeg⁡uxu=[v=1].x_v - \sum_{u \sim v} \frac{1 - r}{\deg u} x_u = [v = 1].

Mỗi lần đứng ở vv dừng với xác suất rr, nên xác suất dừng tại vv là r⋅xvr \cdot x_v.

Bước 1: dựng hệ. Hàng vv có 1 ở cột vv, trừ (1−r)/deg⁡u(1 - r) / \deg u ở cột của mỗi hàng xóm uu, vế phải là 1 khi vv là đỉnh xuất phát.

C++
for (int v = 0; v < n; v++) {
    a[v][v] += 1;
    for (int u : adj[v]) a[v][u] -= (1 - r) / deg[u];
    a[v][n] = v == 0;
}

Bước 2: khử. Gauss-Jordan với trụ lớn nhất như ví dụ đầu bài; f == 0 thì bỏ qua cả hàng, vì ma trận thưa ở những bước đầu. Đỉnh không tới được từ 1 có phương trình không có vế phải và hệ cho xv=0x_v = 0, không cần xử lý riêng.

Bước 3: in. r⋅xvr \cdot x_v; kết quả gần 0 đưa về 0 để khỏi in -0.000000000.

C++
double x = r * a[v][n] / a[v][v];
if (fabs(x) < 5e-10) x = 0;
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const double EPS = 1e-12;
int n, m, deg[305];
double P, Q, a[305][305];
vector<int> adj[305];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> m >> P >> Q;
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        u--, v--;
        adj[u].push_back(v);
        adj[v].push_back(u);
        deg[u]++, deg[v]++;
    }
    double r = P / Q;
    for (int v = 0; v < n; v++) {
        a[v][v] += 1;
        for (int u : adj[v]) a[v][u] -= (1 - r) / deg[u];
        a[v][n] = v == 0;
    }
    for (int c = 0; c < n; c++) {
        int p = c;
        for (int i = c; i < n; i++)
            if (fabs(a[i][c]) > fabs(a[p][c])) p = i;
        swap(a[p], a[c]);
        if (fabs(a[c][c]) < EPS) continue;
        for (int i = 0; i < n; i++)
            if (i != c) {
                double f = a[i][c] / a[c][c];
                if (f == 0) continue;
                for (int j = c; j <= n; j++) a[i][j] -= f * a[c][j];
            }
    }
    cout << fixed << setprecision(9);
    for (int v = 0; v < n; v++) {
        double x = r * a[v][n] / a[v][v];
        if (fabs(x) < 5e-10) x = 0;
        cout << x << '\n';
    }
}
2 1 1 1
1 2

Độ phức tạp: O(N3)O(N^3).

Khi đi thi

  • Số thực: trụ trị tuyệt đối lớn nhất, ngưỡng ε\varepsilon hợp với độ lớn hệ số, kiểm vô nghiệm trước vô số nghiệm, xử lý -0.
  • Mô-đun nguyên tố: khử chính xác bằng nghịch đảo; nghịch đảo ma trận là Gauss-Jordan trên [A∣I][A \mid I]. Mô-đun hợp số: khử kiểu Euclid.
  • XOR, chẵn lẻ: GF(2) với bitset. Xác suất, kỳ vọng trên đồ thị có chu trình: lập hệ xv=[v=s]+∑P⋅xux_v = [v = s] + \sum P \cdot x_u.

Tóm tắt

  • Khử Gauss đưa ma trận về bậc thang trong O(n3)O(n^3); số cột có trụ là hạng.
  • Hàng 0=b≠00 = b \ne 0 là vô nghiệm; hạng nhỏ hơn số ẩn là vô số nghiệm.
  • Nghịch đảo: khử [A∣I][A \mid I]; định thức: tích đường chéo, đổi dấu mỗi lần đổi hàng.
  • GF(2): hàng là bitset, chèn từng phương trình như chèn cơ sở XOR.

Bài tập

Đọc thêm