When the matrix is sparse or has structure, never run elimination: black-box methods (Wiedemann, Lanczos) only use products , and structured matrices (circulant, Vandermonde, tridiagonal, rank-one updates) have closed forms or linear-time solvers. mint from Modular Arithmetic.

MatrixTaskMethodCost
sparse, finite fieldminimal polynomial, solve, determinant, rankWiedemann (Krylov sequence + Berlekamp–Massey), Las Vegas
sparse symmetric, realsextreme eigenvalues, systemsLanczos (three-term recurrence), conjugate gradient for steps
circulant determinant, eigenvalues resultant ; multiply via cyclic convolution (or with FFT over suitable fields)
“permutant” (row = row permuted by )determinant0 unless is one -cycle, else a permuted circulant
determinant
Vandermonde determinant, inverse; columns of the inverse = Lagrange basis polynomials
tridiagonalsolveThomas algorithm (elimination without pivoting)
given inverse updateSherman–Morrison

Wiedemann

Elimination on a sparse matrix fills it in ( time, memory); Wiedemann never modifies the matrix. Pick random , compute the scalars for (one sparse product each), and run Berlekamp–Massey to get the shortest recurrence of : with high probability it is the minimal polynomial of (or of restricted to the Krylov space of ). Then: solve with : if , ; determinant: precondition with a random diagonal so that the minimal and characteristic polynomials of coincide, then ; rank similarly with preconditioners. For and this is feasible where elimination is not. Uses: the linear algebra stage of the quadratic and number field sieves and of index-calculus discrete logs (millions of rows over , block variants), sparse determinants in computer algebra. Block Wiedemann (Coppersmith) uses blocks of vectors: matrix–matrix products with better cache behaviour that parallelise across machines.

vector<mint> berlekampMassey(const vector<mint>& s) {      // shortest c with s[i] = sum_j c[j] s[i-1-j]
    int n = sz(s), L = 0, m = 0; vector<mint> C(n + 1, 0), B(n + 1, 0), T; C[0] = B[0] = 1; mint b = 1;
    for (int i = 0; i < n; i++) {
        m++; mint d = s[i]; for (int j = 1; j <= L; j++) d += C[j] * s[i - j];
        if (d == mint(0)) continue;
        T = C; mint coef = d / b;
        for (int j = m; j <= n; j++) C[j] -= coef * B[j - m];
        if (2 * L > i) continue;
        L = i + 1 - L; B = T; b = d; m = 0;
    }
    vector<mint> c(L); for (int j = 0; j < L; j++) c[j] = -C[j + 1];
    return c;
}
 
struct SparseMat {                                          // n x n, list of (row, col, value)
    int n; vector<tuple<int, int, mint>> e;
    vector<mint> mul(const vector<mint>& v) const { vector<mint> r(n, 0); for (auto& [i, j, x] : e) r[i] += x * v[j]; return r; }
};
 
mt19937_64 wrng(12345);
vector<mint> wiedemannPoly(const SparseMat& A, const vector<mint>& u, vector<mint> v) {   // minimal polynomial of (u^T A^k v), monic, low degree first
    int n = A.n; vector<mint> s(2 * n);
    for (int k = 0; k < 2 * n; k++) { mint t = 0; for (int i = 0; i < n; i++) t += u[i] * v[i]; s[k] = t; v = A.mul(v); }
    auto c = berlekampMassey(s); int L = sz(c); vector<mint> f(L + 1); f[L] = 1;
    for (int j = 0; j < L; j++) f[L - 1 - j] = -c[j];
    return f;
}
 
vector<mint> sparseSolve(const SparseMat& A, const vector<mint>& b, int tries = 3) {   // nonsingular A; {} on failure
    int n = A.n;
    for (int t = 0; t < tries; t++) {
        vector<mint> u(n); for (auto& x : u) x = wrng() % 998244353;
        auto f = wiedemannPoly(A, u, b);
        if (sz(f) < 2 || f[0] == mint(0)) continue;
        vector<mint> x(n, 0), cur = b;                     // x = -(1/f0) sum_{i>=1} f_i A^{i-1} b
        for (int i = 1; i < sz(f); i++) { for (int j = 0; j < n; j++) x[j] += f[i] * cur[j]; cur = A.mul(cur); }
        mint inv = -f[0].inv(); for (auto& y : x) y *= inv;
        if (A.mul(x) == b) return x;
    }
    return {};
}
 
mint sparseDet(const SparseMat& A, int tries = 3) {         // Las Vegas for nonsingular A; returns 0 if every try fails (singular w.h.p.)
    int n = A.n; if (n == 0) return 1;
    for (int t = 0; t < tries; t++) {
        vector<mint> d(n); for (auto& x : d) x = wrng() % 998244352 + 1;
        SparseMat B = A; for (auto& [i, j, x] : B.e) x *= d[j];   // A D
        vector<mint> u(n), v(n); for (auto& x : u) x = wrng() % 998244353; for (auto& x : v) x = wrng() % 998244353;
        auto f = wiedemannPoly(B, u, v);
        if (sz(f) != n + 1) continue;
        mint det = n % 2 ? -f[0] : f[0], dd = 1; for (auto& x : d) dd *= x;
        return det / dd;
    }
    return 0;
}

Structured matrices

template <class T> vector<T> polyStrip(vector<T> a) { while (!a.empty() && a.back() == T(0)) a.pop_back(); return a; }
template <class T> vector<T> polyMod(vector<T> a, vector<T> b) {
    a = polyStrip(a), b = polyStrip(b); assert(!b.empty());
    T inv = T(1) / b.back(); int m = sz(b) - 1;
    for (int n = sz(a) - 1; n >= m; n--) { T x = a[n] * inv; if (x == T(0)) continue; for (int j = 0; j <= m; j++) a[n - m + j] -= x * b[j]; }
    a.resize(min<int>(sz(a), m)); return polyStrip(a);
}
template <class T> T resultant(vector<T> a, vector<T> b) {
    a = polyStrip(a), b = polyStrip(b);
    if (a.empty() || b.empty()) return T(0);
    T res = 1;
    while (true) {
        if (sz(b) == 1) { for (int i = 0; i + 1 < sz(a); i++) res *= b[0]; return res; }   // Res(A, c) = c^deg A
        auto c = polyMod(a, b); if (c.empty()) return T(0);
        int da = sz(a) - 1, db = sz(b) - 1, dc = sz(c) - 1;
        if ((ll)da * db % 2) res = -res;
        for (int i = 0; i < da - dc; i++) res *= b.back();
        a = b, b = c;
    }
}
template <class T> T circulantDeterminant(const vector<T>& c) {     // M[i][j] = c[(j - i) mod n]
    int n = sz(c); if (n == 0) return T(1);
    auto C = polyStrip(c); if (C.empty()) return T(0);
    vector<T> A(n + 1, T(0)); A[0] = T(-1), A[n] = T(1);
    return resultant(A, C);
}
template <class T> T permutantDeterminant(const vector<T>& a, const vector<int>& p) {   // row 0 = a, M[i+1][j] = M[i][p[j]]
    int n = sz(a); if (n == 0) return T(1);
    vector<int> c(n, 0);
    for (int s = 1; s < n; s++) { c[s] = p[c[s - 1]]; if (c[s] == 0) return T(0); }   // more than one cycle: equal rows
    vector<T> b(n); for (int s = 0; s < n; s++) b[s] = a[c[s]];
    vector<char> vis(n, 0); int cycles = 0;
    for (int i = 0; i < n; i++) if (!vis[i]) { cycles++; for (int j = i; !vis[j]; j = c[j]) vis[j] = 1; }
    T d = circulantDeterminant(b);
    return ((n - cycles) + (n - 1) / 2) % 2 ? -d : d;
}
template <class T> T rankOneDeterminant(const vector<T>& a, const vector<T>& b, T x) {   // det(x I + a b^T)
    int n = sz(a); T s = 0, xp = 1;
    for (int i = 0; i < n; i++) s += a[i] * b[i];
    for (int i = 0; i + 1 < n; i++) xp *= x;
    return n == 0 ? T(1) : xp * x + xp * s;
}
template <class T> T vandermondeDeterminant(const vector<T>& x) {
    T d = 1; for (int i = 0; i < sz(x); i++) for (int j = i + 1; j < sz(x); j++) d *= x[j] - x[i]; return d;
}
template <class T> vector<vector<T>> vandermondeInverse(const vector<T>& x) {   // M[i][j] = x_i^j, x distinct
    int n = sz(x); vector<T> full(n + 1, T(0)); full[0] = 1;                  // prod (t - x_i)
    for (int i = 0; i < n; i++) for (int j = n; j >= 0; j--) { full[j] *= -x[i]; if (j) full[j] += full[j - 1]; }
    vector<vector<T>> inv(n, vector<T>(n));
    for (int i = 0; i < n; i++) {
        vector<T> up(n); T rem = full[n];                                       // full / (t - x_i), synthetic division
        for (int j = n - 1; j >= 0; j--) { up[j] = rem; rem = full[j] + rem * x[i]; }
        T down = 1; for (int j = 0; j < n; j++) if (j != i) down *= x[i] - x[j];
        down = T(1) / down;
        for (int j = 0; j < n; j++) inv[j][i] = up[j] * down;
    }
    return inv;
}
template <class T> vector<T> thomas(vector<T> lo, vector<T> di, vector<T> up, vector<T> rhs) {   // lo[i] x[i-1] + di[i] x[i] + up[i] x[i+1] = rhs[i]
    int n = sz(di);
    for (int i = 1; i < n; i++) { T w = lo[i] / di[i - 1]; di[i] -= w * up[i - 1]; rhs[i] -= w * rhs[i - 1]; }
    vector<T> x(n);
    for (int i = n - 1; i >= 0; i--) x[i] = (rhs[i] - (i + 1 < n ? up[i] * x[i + 1] : T(0))) / di[i];
    return x;
}
template <class T> bool shermanMorrison(vector<vector<T>>& B, const vector<T>& u, const vector<T>& v) {   // B = A^-1 becomes (A + u v^T)^-1
    int n = sz(B); vector<T> bu(n, T(0)), vb(n, T(0));
    for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) bu[i] += B[i][j] * u[j], vb[j] += v[i] * B[i][j];
    T den = 1; for (int i = 0; i < n; i++) den += v[i] * bu[i];
    if (den == T(0)) return false;
    T inv = T(1) / den;
    for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) B[i][j] -= bu[i] * vb[j] * inv;
    return true;
}

Lanczos and Krylov methods

For a symmetric and a unit start vector , the Krylov space gets an orthonormal basis from a three-term recurrence

and in that basis becomes the tridiagonal (diagonal , off-diagonal ), whose extreme eigenvalues approximate those of long before (often a few dozen steps for millions of rows). Cost , memory for eigenvalues only.

pair<vector<double>, vector<double>> lanczos(int n, const function<vector<double>(const vector<double>&)>& mulA, int k, mt19937& rng) {
    vector<vector<double>> V; vector<double> al, be;        // full reorthogonalisation: stable, O(k^2 n)
    vector<double> v(n); for (auto& x : v) x = uniform_real_distribution<double>(-1, 1)(rng);
    double nr = 0; for (double x : v) nr += x * x; nr = sqrt(nr); for (auto& x : v) x /= nr;
    for (int j = 0; j < k; j++) {
        V.push_back(v); vector<double> w = mulA(v);
        double a = 0; for (int i = 0; i < n; i++) a += w[i] * v[i]; al.push_back(a);
        for (auto& q : V) { double d = 0; for (int i = 0; i < n; i++) d += w[i] * q[i]; for (int i = 0; i < n; i++) w[i] -= d * q[i]; }
        double b = 0; for (double x : w) b += x * x; b = sqrt(b);
        if (j + 1 == k || b < 1e-10) break;
        be.push_back(b); for (int i = 0; i < n; i++) v[i] = w[i] / b;
    }
    return {al, be};                                        // T: diagonal al, off-diagonal be
}
 
double tridiagLargestEigenvalue(const vector<double>& al, const vector<double>& be) {   // Sturm bisection
    int k = sz(al); double lo = 1e18, hi = -1e18;
    for (int i = 0; i < k; i++) { double r = (i ? fabs(be[i - 1]) : 0) + (i < sz(be) ? fabs(be[i]) : 0); lo = min(lo, al[i] - r); hi = max(hi, al[i] + r); }
    auto below = [&](double x) { int cnt = 0; double d = 1; for (int i = 0; i < k; i++) { d = al[i] - x - (i ? be[i - 1] * be[i - 1] / d : 0); if (d == 0) d = -1e-300; cnt += d < 0; } return cnt; };
    for (int it = 0; it < 200; it++) { double mid = (lo + hi) / 2; if (below(mid) == k) hi = mid; else lo = mid; }
    return hi;
}
LanczosWiedemann
needs symmetryyes (use otherwise)no
iterations
failure modeself-orthogonal vectors over (handled by blocking)unlucky random vectors (retry)
parallelismgood (block Lanczos, Montgomery)excellent (block Wiedemann)
memory vectors scalars
Krylov methodFor
Lanczossymmetric eigenproblems
conjugate gradientsymmetric positive definite systems (Lanczos rearranged)
Arnoldinon-symmetric eigenproblems, full orthogonalisation
GMRESnon-symmetric systems, built on Arnoldi
power iterationthe dominant eigenvalue only; Lanczos is strictly better

In floating point the plain recurrence loses orthogonality once an eigenvalue converges and produces “ghost” copies; the code reorthogonalises fully (fine for small ), real implementations use selective or partial reorthogonalisation; over a finite field the issue disappears. Uses: spectral graph theory (second Laplacian eigenvalue, clustering, expansion), PageRank-type problems, block Lanczos over for factorisation, ground states of sparse Hamiltonians.