All Euler problems
Project Euler

Constrained Permutations

Let A be an n x n matrix with entries in {0, 1}, where A_ij = 1 indicates that element j is permitted in position i. Count the number of permutations sigma in S_n such that A_(i,sigma(i)) = 1 for a...

Source sync Apr 19, 2026
Problem #0631
Level Level 35
Solved By 215
Languages C++, Python
Answer 869588692
Length 271 words
linear_algebraprobabilitymodular_arithmetic

Problem 631: Constrained Permutations

Mathematical Foundation

Theorem 1 (Ryser, 1963). For any n×nn \times n matrix AA,

perm(A)=(1)nS[n](1)Si=1njSAij.\operatorname{perm}(A) = (-1)^n \sum_{S \subseteq [n]} (-1)^{|S|} \prod_{i=1}^{n} \sum_{j \in S} A_{ij}.

Proof. For each subset S[n]S \subseteq [n], expand the product i=1n(jSAij)\prod_{i=1}^{n} \bigl(\sum_{j \in S} A_{ij}\bigr) as a sum over functions f:[n]Sf : [n] \to S:

i=1njSAij=f:[n]Si=1nAi,f(i).\prod_{i=1}^{n} \sum_{j \in S} A_{ij} = \sum_{f : [n] \to S} \prod_{i=1}^{n} A_{i,f(i)}.

Now consider a fixed bijection σSn\sigma \in S_n. The term iAi,σ(i)\prod_{i} A_{i,\sigma(i)} appears in the inner sum whenever range(σ)S\operatorname{range}(\sigma) \subseteq S, i.e., S=[n]S = [n] since σ\sigma is a bijection. So its contribution to the alternating sum is

(1)n(1)niAi,σ(i)=iAi,σ(i).(-1)^n \cdot (-1)^{n} \cdot \prod_i A_{i,\sigma(i)} = \prod_i A_{i,\sigma(i)}.

For a non-injective function f:[n]Sf : [n] \to S with range(f)=r<n|\operatorname{range}(f)| = r < n, the function appears in every Srange(f)S \supseteq \operatorname{range}(f). By inclusion-exclusion, the contribution over all SS is

Srange(f)(1)S=(1)rk=0nr(nrk)(1)k=(1)r0=0\sum_{S \supseteq \operatorname{range}(f)} (-1)^{|S|} = (-1)^r \sum_{k=0}^{n-r} \binom{n-r}{k}(-1)^k = (-1)^r \cdot 0 = 0

since nr1n - r \geq 1. Therefore only bijections survive, yielding (1)n(1)nperm(A)=perm(A)(-1)^n \cdot (-1)^n \operatorname{perm}(A) = \operatorname{perm}(A). \square

Lemma 1 (Gray Code Update). If subsets of [n][n] are enumerated in Gray code order, consecutive subsets differ by exactly one element. The row sums ri(S)=jSAijr_i(S) = \sum_{j \in S} A_{ij} can therefore be updated in O(n)O(n) per transition.

Proof. Let S=S{j0}S' = S \oplus \{j_0\} for some j0j_0. Then ri(S)=ri(S)±Ai,j0r_i(S') = r_i(S) \pm A_{i,j_0} for each i[n]i \in [n], requiring nn additions. The product iri(S)\prod_i r_i(S') is then computed in O(n)O(n). \square

Lemma 2 (Bipartite Matching Equivalence). perm(A)\operatorname{perm}(A) equals the number of perfect matchings in the bipartite graph G=(UV,E)G = (U \cup V, E) where U={u1,,un}U = \{u_1, \ldots, u_n\} (positions), V={v1,,vn}V = \{v_1, \ldots, v_n\} (elements), and (ui,vj)E(u_i, v_j) \in E iff Aij=1A_{ij} = 1.

Proof. A perfect matching MM in GG is a bijection σ\sigma with (ui,vσ(i))E(u_i, v_{\sigma(i)}) \in E for all ii, i.e., Ai,σ(i)=1A_{i,\sigma(i)} = 1 for all ii. Summing over all such bijections gives perm(A)\operatorname{perm}(A). \square

Editorial

Compute the permanent of a 0-1 matrix using Ryser’s formula: perm(A) = (-1)^n * sum_{S} (-1)^|S| * prod_i (sum_{j in S} A_ij) Method 1: Ryser’s formula O(2^n * n) Method 2: Brute force O(n! * n) (verification). We ryser’s formula with Gray code enumeration. Finally, else.

Pseudocode

Ryser's formula with Gray code enumeration
if j0 was added to S
else

Complexity Analysis

  • Time: O(2nn)O(2^n \cdot n). There are 2n2^n subsets; each transition updates nn row sums and computes an nn-fold product.
  • Space: O(n)O(n) for storing the row sums and the matrix AA.

For comparison, the naive summation over all n!n! permutations costs O(n!n)O(n! \cdot n).

Answer

869588692\boxed{869588692}

Code

Each problem page includes the exact C++ and Python source files from the local archive.

C++ project_euler/problem_631/solution.cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

/*
 * Problem 631: Constrained Permutations
 *
 * Compute the permanent of a 0-1 matrix via Ryser's formula:
 * perm(A) = (-1)^n * sum_{S} (-1)^|S| * prod_i (sum_{j in S} A_ij)
 *
 * Complexity: O(2^n * n)
 */

ll permanent_ryser(vector<vector<int>>& A) {
    int n = A.size();
    ll total = 0;
    for (int mask = 0; mask < (1 << n); mask++) {
        int bits = __builtin_popcount(mask);
        ll prod = 1;
        for (int i = 0; i < n; i++) {
            int s = 0;
            for (int j = 0; j < n; j++)
                if (mask & (1 << j))
                    s += A[i][j];
            prod *= s;
        }
        if ((n - bits) % 2 == 0) total += prod;
        else total -= prod;
    }
    if (n % 2 == 1) total = -total;
    return total;
}

// Brute force (verification for small n)
ll permanent_brute(vector<vector<int>>& A) {
    int n = A.size();
    vector<int> perm(n);
    iota(perm.begin(), perm.end(), 0);
    ll total = 0;
    do {
        ll prod = 1;
        for (int i = 0; i < n; i++) prod *= A[i][perm[i]];
        total += prod;
    } while (next_permutation(perm.begin(), perm.end()));
    return total;
}

int main() {
    // Test: identity matrix
    vector<vector<int>> I3 = {{1,0,0},{0,1,0},{0,0,1}};
    assert(permanent_ryser(I3) == 1);
    assert(permanent_brute(I3) == 1);

    // Test: all-ones matrix
    vector<vector<int>> J3 = {{1,1,1},{1,1,1},{1,1,1}};
    assert(permanent_ryser(J3) == 6);
    assert(permanent_brute(J3) == 6);

    // Random tests
    srand(42);
    for (int trial = 0; trial < 50; trial++) {
        int n = 2 + rand() % 5;
        vector<vector<int>> A(n, vector<int>(n));
        for (int i = 0; i < n; i++)
            for (int j = 0; j < n; j++)
                A[i][j] = rand() % 2;
        assert(permanent_ryser(A) == permanent_brute(A));
    }

    cout << "All verifications passed." << endl;
    return 0;
}