← All Posts
Combinatorics· Part 4 of 16

Permanent of a Matrix

Permanent vs Determinant

Given an n × n matrix A, its permanent is defined as:

perm(A) = Σ_{σ ∈ S_n} Π_{i=1}^{n} a_{i, σ(i)}

Compare this with the determinant:

det(A) = Σ_{σ ∈ S_n} sgn(σ) ⋅ Π_{i=1}^{n} a_{i, σ(i)}

The only difference is the sign factor sgn(σ). The determinant alternates signs based on whether the permutation is even or odd; the permanent simply sums every product without any sign. This tiny difference has enormous computational consequences.

Determinant

  • Includes sgn(σ) factor
  • Computable in O(n³) via Gaussian elimination
  • Invariant under row/column operations (up to sign)
  • Has deep algebraic structure (eigenvalues, etc.)

Permanent

  • No sign factor — all terms positive
  • Best known: O(2n ⋅ n) (Ryser’s formula)
  • No known efficient row-reduction approach
  • Counts combinatorial objects (matchings)

Let’s see a concrete 3×3 example. Consider:

    | 1  2  3 |
A = | 4  5  6 |
    | 7  8  9 |

det(A) = 1(5⋅9 - 6⋅8) - 2(4⋅9 - 6⋅7) + 3(4⋅8 - 5⋅7)
       = 1(-3) - 2(-6) + 3(-3) = -3 + 12 - 9 = 0

perm(A) = 1⋅5⋅9 + 1⋅6⋅8 + 2⋅4⋅9 + 2⋅6⋅7 + 3⋅4⋅8 + 3⋅5⋅7
        = 45 + 48 + 72 + 84 + 96 + 105 = 450

Notice: the determinant vanishes (the matrix is singular), yet the permanent is a large positive number. The two quantities carry fundamentally different information.

Theorem (Valiant, 1979). Computing the permanent of a 0/1 matrix is #P-complete. Informally, this means it is at least as hard as counting the solutions to any NP problem. Unless P = #P, no polynomial-time algorithm exists.

This hardness result is one of the most striking in complexity theory. The determinant lives in P (even in NC, meaning it is efficiently parallelizable), while the permanent — differing by only a sign — is #P-complete. This is sometimes called the “permanent vs determinant” problem and remains a central open question in algebraic complexity.

For competitive programming, this means we must rely on exponential-time algorithms. Fortunately, Ryser’s formula gives us an efficient O(2n ⋅ n) method that works well for n ≤ 20.

Ryser’s Formula

The fastest known general algorithm for computing the permanent is Ryser’s formula (1963). It expresses the permanent as a sum over all subsets of columns:

perm(A) = (-1)^n Σ_{S ⊆ [n]} (-1)^{|S|} Π_{i=1}^{n} (Σ_{j ∈ S} a_{ij})

Here [n] = {1, 2, …, n} and the sum ranges over all 2n subsets of columns. For each subset S, we compute the product of row sums restricted to columns in S.

Derivation via Inclusion-Exclusion

The key insight is to view the permanent as a sum over all functions f: [n] → [n] (not just bijections), then subtract the overcounting using inclusion-exclusion.

Consider expanding the product:

Π_{i=1}^{n} (Σ_{j=1}^{n} a_{ij}) = Σ_{f: [n] → [n]} Π_{i=1}^{n} a_{i, f(i)}

This sums over all nn functions from rows to columns. The permanent only wants bijections (permutations). We use inclusion-exclusion to enforce that every column is hit at least once.

Define Aj = “column j is not used by f.” We want to count functions avoiding all Aj, i.e., functions that use every column:

perm(A) = |complement of (A_1 ∪ A_2 ∪ … ∪ A_n)|
        = Σ_{S ⊆ [n]} (-1)^{|S|} ⋅ (functions avoiding columns in S)
        = Σ_{S ⊆ [n]} (-1)^{n - |S|} ⋅ Π_{i=1}^{n} (Σ_{j ∈ S} a_{ij})

Reindexing by the complement of S gives us Ryser’s formula. The factor (-1)n at the front accounts for the sign convention.

Complexity. Ryser’s formula iterates over 2n subsets and does O(n) work per subset, giving O(2n ⋅ n) time. With Gray code enumeration (flipping one element at a time), we can reduce the per-subset work to O(1) amortized.

Step-by-Step Example (2×2)

Let’s verify with a 2×2 matrix:

A = | a  b |
    | c  d |

perm(A) = a⋅d + b⋅c   (by definition: two permutations of {1,2})

Ryser: (-1)^2 Σ_{S ⊆ {1,2}} (-1)^{|S|} Π_{i=1}^{2} (row_i sum over S)

S = {}:    (-1)^0 ⋅ 0 ⋅ 0 = 0
S = {1}:   (-1)^1 ⋅ a ⋅ c = -ac
S = {2}:   (-1)^1 ⋅ b ⋅ d = -bd
S = {1,2}: (-1)^2 ⋅ (a+b)(c+d) = ac + ad + bc + bd

Total = 0 - ac - bd + ac + ad + bc + bd = ad + bc  ✓

Bitmask DP Implementation

In competitive programming, we implement the permanent using bitmask DP over columns. The idea is to process rows one by one and track which columns have been “assigned” so far using a bitmask.

DP Formulation

Let dp[mask] = number of ways to assign columns (encoded in mask) to the first popcount(mask) rows such that each row gets a distinct column. Then:

dp[0] = 1
dp[mask] = Σ_{j ∈ mask} a[popcount(mask)-1][j] ⋅ dp[mask ^ (1 << j)]

Answer = dp[(1 << n) - 1]

This directly counts perfect matchings: at each step, we pick which column to match to the current row.

Complexity Warning. This DP has O(2n ⋅ n) time and O(2n) space. For n = 20, that’s about 20 million states — feasible. For n = 25, it’s over 800 million — likely TLE.

C++ Implementation: Bitmask DP

#include <bits/stdc++.h>
using namespace std;

// Compute permanent of an n x n matrix using bitmask DP.
// O(2^n * n) time, O(2^n) space.
// Works for n <= 20 within typical contest limits.
long long permanent_bitmask(vector<vector<long long>>& a, int n) {
    vector<long long> dp(1 << n, 0);
    dp[0] = 1;

    for (int mask = 1; mask < (1 << n); mask++) {
        int row = __builtin_popcount(mask) - 1;
        for (int j = 0; j < n; j++) {
            if (mask & (1 << j)) {
                dp[mask] += a[row][j] * dp[mask ^ (1 << j)];
            }
        }
    }
    return dp[(1 << n) - 1];
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;

    vector<vector<long long>> a(n, vector<long long>(n));
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            cin >> a[i][j];

    cout << permanent_bitmask(a, n) << "\n";
    return 0;
}

This bitmask DP is the most intuitive approach and directly models the assignment/matching interpretation. Each state dp[mask] represents a partial matching of the first popcount(mask) rows to the columns indicated by mask.

C++ Implementation: Ryser’s Formula

Ryser’s formula is often faster in practice because it avoids the overhead of the DP transitions and has better cache behavior:

#include <bits/stdc++.h>
using namespace std;

// Compute permanent using Ryser's formula.
// O(2^n * n) time, O(n) space.
long long permanent_ryser(vector<vector<long long>>& a, int n) {
    long long result = 0;

    // Iterate over all non-empty subsets S of [n]
    for (int S = 1; S < (1 << n); S++) {
        long long prod = 1;
        for (int i = 0; i < n; i++) {
            long long row_sum = 0;
            for (int j = 0; j < n; j++) {
                if (S & (1 << j)) {
                    row_sum += a[i][j];
                }
            }
            prod *= row_sum;
        }

        int bits = __builtin_popcount(S);
        if ((n - bits) % 2 == 0)
            result += prod;
        else
            result -= prod;
    }

    // Multiply by (-1)^n
    if (n % 2 == 1)
        result = -result;

    return result;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;

    vector<vector<long long>> a(n, vector<long long>(n));
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            cin >> a[i][j];

    cout << permanent_ryser(a, n) << "\n";
    return 0;
}

A key advantage of Ryser’s formula is that it uses only O(n) auxiliary space (versus O(2n) for the bitmask DP), which matters when n is close to 20.

Gray Code Optimization

We can speed up Ryser’s formula by enumerating subsets in Gray code order, so that consecutive subsets differ by exactly one element. Instead of recomputing all row sums from scratch for each subset, we incrementally update them by adding or subtracting one column:

permanent_ryser_gray(A, n):
  row_sum[0..n-1] ← 0
  result ← 0
  for S ← 1 to 2^n - 1:
    j ← bit_changed(S) // which column flipped
    sign ← +1 if column j was added, -1 if removed
    for i ← 0 to n-1:
      row_sum[i] += sign ⋅ A[i][j]
    prod ← Π row_sum[i]
    result += (-1)^(n - popcount(S)) ⋅ prod
  return (-1)^n ⋅ result

With Gray code enumeration, the per-subset update cost drops from O(n) (recomputing row sums) to O(1) for the update step, though we still need O(n) to compute the product. Total: still O(2n ⋅ n), but the constant factor is significantly better.

Permanent and Perfect Matchings

The most important combinatorial interpretation of the permanent is through bipartite graph matchings.

Theorem. Let G = (U ∪ V, E) be a bipartite graph with |U| = |V| = n. Let A be the biadjacency matrix where aij = 1 if (ui, vj) ∈ E, and 0 otherwise. Then perm(A) = number of perfect matchings in G.

This follows directly from the definition. Each permutation σ ∈ Sn corresponds to a potential matching {(u1, vσ(1)), …, (un, vσ(n))}. The product Π ai,σ(i) is 1 if and only if all edges in this matching exist, and 0 otherwise. So the sum over all permutations counts exactly the perfect matchings.

Weighted Matchings

If the matrix entries are weights rather than 0/1 values, then perm(A) computes the sum of weights of all perfect matchings, where the weight of a matching is the product of its edge weights. This generalizes the counting interpretation.

Connection to the Assignment Problem

The assignment problem asks for the minimum-weight perfect matching in a bipartite graph. While it can be solved in O(n³) by the Hungarian algorithm, counting all perfect matchings (or summing all matching weights) is #P-hard, which is why we need exponential algorithms.

Problem Complexity Algorithm
Does a perfect matching exist? P Hopcroft-Karp, O(E√V)
Min-weight perfect matching P Hungarian algorithm, O(n³)
Count perfect matchings (0/1) #P-complete Ryser’s formula, O(2nn)
Sum of matching weights #P-hard Ryser / bitmask DP, O(2nn)

Hall’s Marriage Theorem

Hall’s theorem gives a necessary and sufficient condition for the existence of a perfect matching in a bipartite graph: for every subset S of the left vertices, the neighborhood N(S) satisfies |N(S)| ≥ |S|.

The permanent provides a quantitative strengthening: not just whether a perfect matching exists, but how many. A celebrated result by van der Waerden (proved by Egorychev and Falikman in 1981) gives a lower bound on the permanent of a doubly stochastic matrix:

Van der Waerden’s Conjecture (proved 1981). For any n × n doubly stochastic matrix A, perm(A) ≥ n!/nn, with equality iff A is the matrix with all entries 1/n.

Inclusion-Exclusion Perspective

Let’s re-derive Ryser’s formula from scratch using a pure inclusion-exclusion argument, to build intuition for why it works.

The Overcounting Approach

We want to count the number of permutation matrices “contained” in A (i.e., supported on its nonzero entries). Start by overcounting: consider all generalized assignments f: [n] → [n] (not necessarily injective).

For each row i, independently choose any column j with aij ≠ 0. The total number of such assignments is:

T = Π_{i=1}^{n} r_i   where r_i = Σ_{j=1}^{n} a_{ij} (row sum)

This overcounts because multiple rows may choose the same column. We need to subtract assignments that miss at least one column.

Applying Inclusion-Exclusion

Let Bj = set of assignments that do not use column j. The permanent equals the number of assignments that avoid all Bj:

perm(A) = |T \ (B_1 ∪ B_2 ∪ … ∪ B_n)|

By inclusion-exclusion:

perm(A) = Σ_{k=0}^{n} (-1)^k Σ_{|T|=k} |B_{t_1} ∩ B_{t_2} ∩ … ∩ B_{t_k}|

The intersection B_{t_1} ∩ … ∩ B_{t_k} consists of assignments that avoid columns t1, …, tk. For such assignments, each row i picks from the remaining columns, so:

|B_{t_1} ∩ … ∩ B_{t_k}| = Π_{i=1}^{n} (Σ_{j ∉ {t_1,...,t_k}} a_{ij})

Substituting into the inclusion-exclusion sum and reindexing by the complement set S = [n] \ {t1, …, tk} (the columns we do include):

perm(A) = Σ_{S ⊆ [n]} (-1)^{n-|S|} Π_{i=1}^{n} (Σ_{j ∈ S} a_{ij})
        = (-1)^n Σ_{S ⊆ [n]} (-1)^{|S|} Π_{i=1}^{n} (Σ_{j ∈ S} a_{ij})

This is exactly Ryser’s formula. ■

Why This Matters in CP

The inclusion-exclusion viewpoint shows that the permanent is a special case of a general pattern: count unrestricted objects, then subtract those violating constraints. Many CP problems follow this template:

Whenever you see “count bijections with constraints,” think permanent.

CP Applications

1. Counting Perfect Matchings in Bipartite Graphs

The most direct application. Given a bipartite graph with n ≤ 20 on each side, build the adjacency matrix and compute the permanent.

#include <bits/stdc++.h>
using namespace std;

const int MOD = 998244353;

// Permanent modulo a prime using Ryser's formula.
// Handles n <= 20 comfortably.
long long permanent_mod(vector<vector<int>>& a, int n, int mod) {
    long long result = 0;

    for (int S = 1; S < (1 << n); S++) {
        long long prod = 1;
        for (int i = 0; i < n; i++) {
            long long row_sum = 0;
            for (int j = 0; j < n; j++) {
                if (S & (1 << j)) {
                    row_sum = (row_sum + a[i][j]) % mod;
                }
            }
            prod = prod % mod * (row_sum % mod) % mod;
        }

        int bits = __builtin_popcount(S);
        if ((n - bits) % 2 == 0)
            result = (result + prod) % mod;
        else
            result = (result - prod + mod) % mod;
    }

    if (n % 2 == 1)
        result = (mod - result) % mod;

    return result;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m;
    cin >> n >> m;

    vector<vector<int>> adj(n, vector<int>(n, 0));
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        adj[u][v] = 1;
    }

    cout << permanent_mod(adj, n, MOD) << "\n";
    return 0;
}

2. Counting Latin Squares

A Latin square of order n is an n × n grid filled with n different symbols such that each symbol occurs exactly once in each row and column. We can build a Latin square row by row: after placing the first k rows, the allowed positions in row k+1 form a 0/1 matrix, and the number of valid completions of that row is a permanent.

The total number of Latin squares can be computed by multiplying permanents across rows, though the matrix changes after each row is fixed. For small n, this gives an exact count.

3. System of Distinct Representatives (SDR)

Given sets S1, S2, …, Sn over a universe of size n, an SDR is a selection of distinct elements x1 ∈ S1, x2 ∈ S2, …, xn ∈ Sn. The number of SDRs equals perm(A) where aij = 1 iff element j is in set Si.

4. Common Contest Patterns

Look for the permanent in problems that ask:

Constraint Check. Always verify the matrix dimension. If n ≤ 20, permanent is feasible. If n ≤ 25 with careful optimization (Gray code + bitwise tricks), it might pass. For n > 25, you likely need a different approach (e.g., polynomial-time algorithms for special matrix structures, or approximation).

5. Problem References

Problem Key Idea n
SPOJ DPER Direct permanent computation ≤ 20
Codeforces 895C Bitmask DP for counting products with distinct primes ≤ 20
AtCoder ABC226F Perfect matching count in bipartite graph ≤ 17
Project Euler #434 Rigid graphs & permanent computation varies

Optimizations

Glynn’s Formula (2010)

Glynn’s formula provides an alternative to Ryser that avoids the alternating signs, using ±1 vectors instead of subset sums:

perm(A) = (1 / 2^{n-1}) Σ_{\varepsilon} (Π_{j=1}^{n} &varepsilon_j) ⋅ Π_{i=1}^{n} (Σ_{j=1}^{n} &varepsilon_j ⋅ a_{ij})

where the sum is over all vectors ϵ ∈ {+1, -1}n with ϵ1 = +1 (we fix one coordinate to avoid double-counting).

Advantages of Glynn’s formula:

When to use Glynn vs Ryser: For standard contest problems with integer or modular arithmetic, Ryser is usually simpler and sufficient. Use Glynn when the modulus is 2 (where Ryser’s (-1)k terms collapse), or when working over fields of characteristic 2.

Parallel / SIMD Computation

Since each subset’s contribution in Ryser’s formula is independent, the computation is embarrassingly parallel. Practical speedups:

Modular Permanent

When computing perm(A) mod p for a prime p, all arithmetic is done in Zp. Key considerations:

Special case: mod 2. perm(A) ≡ det(A) (mod 2). So over GF(2), you don’t need Ryser at all — just use Gaussian elimination in O(n³). This trick has appeared in several contest problems.

Sparse Matrix Optimization

If the matrix A is sparse (many zeros), the bitmask DP can be sped up. For each row, only iterate over columns where aij ≠ 0. If each row has at most d nonzero entries, the DP transition cost drops from O(n) to O(d) per state.

For very sparse bipartite graphs (small degree), this can be a significant improvement, though the number of DP states remains 2n.

Comparison of Methods

Method Time Space Best For
Naïve (all permutations) O(n! ⋅ n) O(n) n ≤ 10, sanity checks
Bitmask DP O(2n ⋅ n) O(2n) n ≤ 20, intuitive
Ryser’s formula O(2n ⋅ n) O(n) n ≤ 23, low memory
Ryser + Gray code O(2n ⋅ n) O(n) n ≤ 25, fast constant
Glynn’s formula O(2n-1 ⋅ n) O(n) char 2, numerical

Summary

The permanent of a matrix is a deceptively simple quantity — identical to the determinant except for a missing sign — yet it is fundamentally harder to compute. Here are the key takeaways:

Rule of thumb: If a CP problem asks you to count perfect matchings or constrained bijections and n ≤ 20, reach for perm(A) via Ryser’s formula or bitmask DP. If the answer is needed mod 2, use Gaussian elimination on the determinant instead.