The definition has terms; elimination computes the determinant in because row operations change it predictably. The permanent drops the signs and becomes p-hard; the best general method is Ryser’s . mint from Modular Arithmetic.

Row operationEffect on
swap two rows
multiply a row by
add a multiple of one row to anotherunchanged
Property
; multiplicative
;
iff the rows are linearly dependent (singular)
triangular or block-triangularproduct of the diagonal entries / block determinants
(eigenvalues with multiplicity)constant term of the characteristic polynomial
Hadamard: for number of CRT primes for an exact answer
SettingMethodCost
modulo a primeelimination with inverses
modulo a composite (no inverses)Euclid-style row reduction
exact integersBareiss (fraction-free) or several primes + CRT with the Hadamard bound operations on -bit numbers
floating pointLU with partial pivoting
sparseWiedemann (Sparse Matrices)
Where determinants appear
spanning treesMatrix-Tree theorem: a Laplacian cofactor (Matrix-Tree Theorem)
Eulerian circuitsBEST theorem: arborescences
perfect matching exists (general graph)Tutte matrix rank (random)
perfect matchings of a planar graphFKT: a Pfaffian,
perfect matchings of a bipartite graphthe permanent (#P-hard); parity =
non-intersecting lattice pathsLindström–Gessel–Viennot: a determinant of path counts
lattice volume, orientation, in-circle testssmall determinants (Geometry Primitives)
Cramer’s rule : , use elimination instead
mint detMod(vector<vector<mint>> a) {                       // prime modulus, O(n^3)
    int n = sz(a); mint det = 1;
    for (int i = 0; i < n; i++) {
        int piv = -1; for (int r = i; r < n; r++) if (a[r][i] != mint(0)) { piv = r; break; }
        if (piv < 0) return 0;
        if (piv != i) swap(a[piv], a[i]), det = -det;
        det *= a[i][i]; mint inv = a[i][i].inv();
        for (int r = i + 1; r < n; r++) { mint f = a[r][i] * inv; if (f == mint(0)) continue; for (int c = i; c < n; c++) a[r][c] -= f * a[i][c]; }
    }
    return det;
}
 
ll detComposite(vector<vector<ll>> a, ll m) {              // any modulus m >= 1, entries in [0, m)
    int n = sz(a); ll det = 1 % m;
    auto mulm = [&](ll x, ll y) { return (ll)((__int128)x * y % m); };
    for (int i = 0; i < n; i++) {
        for (int r = i + 1; r < n; r++)
            while (a[r][i]) {                               // Euclid on column i: a[i][i] <- a[i][i] mod a[r][i], then swap
                ll t = a[i][i] / a[r][i];
                for (int c = i; c < n; c++) a[i][c] = ((a[i][c] - mulm(t, a[r][c])) % m + m) % m;
                swap(a[i], a[r]); det = (m - det) % m;
            }
        det = mulm(det, a[i][i]);
        if (!det) return 0;
    }
    return det;
}
 
i128 bareiss(vector<vector<i128>> a) {                       // exact integer determinant; intermediates are minors and products of two minors must fit in i128
    int n = sz(a); if (n == 0) return 1;
    i128 prev = 1; int sign = 1;
    for (int k = 0; k < n - 1; k++) {
        if (a[k][k] == 0) {
            int p = -1; for (int r = k + 1; r < n; r++) if (a[r][k] != 0) { p = r; break; }
            if (p < 0) return 0;
            swap(a[k], a[p]); sign = -sign;
        }
        for (int i = k + 1; i < n; i++) for (int j = k + 1; j < n; j++) a[i][j] = (a[i][j] * a[k][k] - a[i][k] * a[k][j]) / prev;   // exact
        prev = a[k][k];
    }
    return sign * a[n - 1][n - 1];
}
 
mint lgvGrid(const vector<pair<int, int>>& src, const vector<pair<int, int>>& dst, const Comb& cb) {   // vertex-disjoint up/right path systems src[i] -> dst[i]
    int k = sz(src); vector<vector<mint>> M(k, vector<mint>(k));
    for (int i = 0; i < k; i++) for (int j = 0; j < k; j++) {
        int dx = dst[j].first - src[i].first, dy = dst[j].second - src[i].second;
        M[i][j] = dx < 0 || dy < 0 ? mint(0) : cb.C(dx + dy, dx);
    }
    return detMod(M);
}

Lindström–Gessel–Viennot. In a DAG, with sources and sinks , (path counts) is the signed sum over vertex-disjoint path systems of . When every disjoint system must use the identity (e.g. sources and sinks in “compatible” order on the boundary of a planar grid), the determinant counts the non-intersecting systems directly: several hard-looking counting problems become one determinant.

Permanent and hafnian

QuantityCountsAlgorithm
weighted perfect matchings of a bipartite graph; permutations avoiding forbidden cellsRyser with Gray code, ; mod 2 equals
( symmetric, even)weighted perfect matchings of a general graphBjörklund’s algorithm, ( in about 2 s)
values of over all subset DP with bitsets,

The permanent is p-complete (Valiant), even for 0/1 matrices; the signs are exactly what makes the determinant easy. only for special .

template <class T> T permanent(const vector<vector<T>>& a) {   // Ryser, any commutative ring, n <= ~25
    int n = sz(a); if (n == 0) return T(1);
    vector<T> rowSum(n, T(0)); T total = T(0); unsigned gray = 0;
    for (unsigned k = 1; k < (1u << n); k++) {
        unsigned g = k ^ (k >> 1); int j = __builtin_ctz(g ^ gray); bool add = g >> j & 1;
        for (int i = 0; i < n; i++) rowSum[i] = add ? rowSum[i] + a[i][j] : rowSum[i] - a[i][j];
        gray = g;
        T prod = T(1); for (int i = 0; i < n; i++) prod = prod * rowSum[i];
        if (__builtin_popcount(g) % 2 == n % 2) total = total + prod; else total = total - prod;
    }
    return total;
}
 
template <class Z> Z hafnian(const vector<vector<Z>>& a) {   // symmetric, n even, diagonal ignored (Bjorklund 2012)
    int n = sz(a); assert(n % 2 == 0); int h = n / 2 + 1; using P = vector<Z>;
    auto addMul = [&](P& x, const P& p, const P& q) { for (int i = 0; i < h; i++) { if (p[i] == Z(0)) continue; for (int j = 0; j + i + 1 < h; j++) x[i + j + 1] += p[i] * q[j]; } };   // x += t p q
    auto solve = [&](auto&& self, const vector<vector<P>>& v) -> P {   // v[i][j], j < i
        P ans(h, Z(0));
        if (v.empty()) { ans[0] = 1; return ans; }
        int m = sz(v) - 2; auto V = v; V.resize(m);
        P zero = self(self, V);
        for (int i = 0; i < m; i++) for (int j = 0; j < i; j++) { addMul(V[i][j], v[m][i], v[m + 1][j]); addMul(V[i][j], v[m + 1][i], v[m][j]); }
        P one = self(self, V);
        for (int i = 0; i < h; i++) ans[i] += one[i] - zero[i];
        addMul(ans, one, v[m + 1][m]);
        return ans;
    };
    vector<vector<P>> v(n);
    for (int i = 0; i < n; i++) { v[i].resize(i); for (int j = 0; j < i; j++) v[i][j] = P(h, Z(0)), v[i][j][0] = a[i][j]; }
    return solve(solve, v).back();
}
 
template <int V> bitset<V> permutationXorValues(const vector<vector<int>>& a) {   // entries < V
    int n = sz(a); vector<bitset<V>> dp(1 << n); dp[0][0] = 1;
    for (int mask = 0; mask < (1 << n); mask++) {
        int i = __builtin_popcount(mask); if (i == n || dp[mask].none()) continue;
        for (int j = 0; j < n; j++) if (!(mask >> j & 1)) {
            auto& to = dp[mask | 1 << j];
            for (int x = dp[mask]._Find_first(); x < V; x = dp[mask]._Find_next(x)) to[x ^ a[i][j]] = 1;
        }
    }
    return dp[(1 << n) - 1];
}