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 operation | Effect on |
|---|---|
| swap two rows | |
| multiply a row by | |
| add a multiple of one row to another | unchanged |
| Property | |
|---|---|
| ; | multiplicative |
| ; | |
| iff the rows are linearly dependent (singular) | |
| triangular or block-triangular | product 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 |
| Setting | Method | Cost |
|---|---|---|
| modulo a prime | elimination with inverses | |
| modulo a composite (no inverses) | Euclid-style row reduction | |
| exact integers | Bareiss (fraction-free) or several primes + CRT with the Hadamard bound | operations on -bit numbers |
| floating point | LU with partial pivoting | |
| sparse | Wiedemann (Sparse Matrices) |
| Where determinants appear | |
|---|---|
| spanning trees | Matrix-Tree theorem: a Laplacian cofactor (Matrix-Tree Theorem) |
| Eulerian circuits | BEST theorem: arborescences |
| perfect matching exists (general graph) | Tutte matrix rank (random) |
| perfect matchings of a planar graph | FKT: a Pfaffian, |
| perfect matchings of a bipartite graph | the permanent (#P-hard); parity = |
| non-intersecting lattice paths | Lindström–Gessel–Viennot: a determinant of path counts |
| lattice volume, orientation, in-circle tests | small 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.
Proof: elimination, multiplicativity, Hadamard, composite moduli, Bareiss, LGV
Row operations. is multilinear and alternating in the rows (Leibniz formula): swapping two rows composes every with a transposition (sign flip); scaling a row scales every term; adding times row to row adds (matrix with two equal rows) . Elimination reaches an upper-triangular matrix, whose only non-zero Leibniz term is the diagonal. Multiplicativity: for fixed , the rows of are , so is multilinear and alternating in the rows of , hence equals with . Transpose: preserves the sign. Singular: elimination produces a zero row iff the rows are dependent. Block triangular: a permutation with a non-zero term must map the first block of indices into itself. Hadamard: Gram–Schmidt writes with unit lower triangular and with orthogonal rows , ; .
Composite modulus. The inner loop is the Euclidean algorithm on the pair using only “subtract a multiple of a row” (determinant unchanged) and swaps (sign flip); since in column no modular wrap occurs there, it terminates with after rounds, and the matrix becomes upper triangular. Bareiss. Let be the leading minor and the entries after steps of ordinary elimination; by the block formula , (a Schur complement entry) equals the bordered minor (rows , columns ) divided by . Put . Then , and the elimination step multiplied by becomes , the Bareiss recurrence with (
prev). Every is a minor of the integer matrix, so the divisions are exact and, by Hadamard, the values have bits. Row swaps apply the argument to the permuted matrix.LGV. Expand as a signed sum over path systems. For a system with an intersection, take the smallest index whose path meets another path, the first vertex on it shared with another path, and the smallest whose path passes ; swapping the tails of paths and after is an involution on intersecting systems (the same , , are chosen again) that composes with a transposition, so these terms cancel in pairs. All functions were checked by brute force (Leibniz over permutations, composite moduli 12, 36, , exact values against
__int128Leibniz, and enumerated disjoint path pairs and triples in small grids).
Permanent and hafnian
| Quantity | Counts | Algorithm |
|---|---|---|
| weighted perfect matchings of a bipartite graph; permutations avoiding forbidden cells | Ryser with Gray code, ; mod 2 equals | |
| ( symmetric, even) | weighted perfect matchings of a general graph | Bjö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];
}Proof: Ryser, Björklund's hafnian, XOR values
Ryser. sums over all functions ; inclusion–exclusion over the set of columns misses keeps exactly the surjective , which for are the permutations: . The Gray code changes one column per step, updating every row sum in (Inclusion–Exclusion).
Hafnian (Björklund 2012). Fix the pairing of the vertices. For a perfect matching , splits into disjoint closed alternating cycles that together visit every pair exactly once, and is recovered from them. The recursion removes the last pair in two ways:
zerodeletes it (the structure avoids the pair),onekeeps it by contracting “matching edge into , pair edge, matching edge out of ” into a single edge whose polynomial degree records one more matching edge, plus the term for the 2-cycle closing on the pair itself. Unfolding over all pairs, the result is , where is the generating polynomial (by number of matching edges, variable ) of collections of closed alternating walks through the pairs of only. By inclusion–exclusion over the set of pairs left untouched, the alternating sum keeps exactly the collections that touch every pair. A collection with exactly matching edges that touches all pairs touches each pair exactly once, so it is the cycle decomposition of for a perfect matching with weight : the coefficient of (.back(), all polynomials truncated at degree ) is . The recursion has leaves, and each level does polynomial products of length , giving . XOR values: the DP assigns rows in order to unused columns, sodp[mask]is the set of XORs over all partial assignments of rows onto the columns inmask. All three were checked against brute force over all permutations / perfect matchings for .
Related notes
- Gaussian Elimination
- Matrix-Tree Theorem (exact spanning tree counts with Bareiss)
- General Matching (Tutte matrix)