Skip to content
The Lumenary

Math laboratory

Exponential generating functions versus brute-force counting

Expands the EGFs exp(e^x - 1), exp(x + x^2/2) and e^(-x)/(1 - x) with exact fractions and compares their coefficients with brute-force counts of set partitions, involutions and derangements for n <= 7, then checks |D(n)/n! - 1/e| <= 1/(n+1)! numerically. Exact for the sizes shown.

Back to the labExact computationExhaustive finite checkNumerical approximation
Exponential generating functions versus brute-force countingPythonExact computationExhaustive finite checkNumerical approximationComputationally tested
"""Exponential generating functions, computed exactly and checked by brute force.

A labelled structure on n points is counted by a_n, and its exponential
generating function (EGF) is A(x) = sum a_n x^n / n!. The exponential
formula says: if a structure is a set of "connected" pieces counted by
C(x), then A(x) = exp(C(x)). We expand three EGFs with exact fractions:

* set partitions:   exp(e^x - 1)        (blocks are nonempty sets)
* involutions:      exp(x + x^2/2)      (cycles of length 1 or 2)
* derangements:     e^(-x) / (1 - x)    (permutations with no fixed point)

and compare their coefficients with direct enumeration for n <= 7. We also
watch D(n)/n! approach 1/e, with the alternating-series error bound.
"""

import itertools
import math
from fractions import Fraction

N = 12

def series_mul(a, b):
    """Product of two truncated power series (lists of Fractions)."""
    return [sum(a[i] * b[n - i] for i in range(n + 1)) for n in range(len(a))]

def series_exp(f):
    """exp(f) for a truncated series with f[0] = 0, via g' = f' g:
    n g_n = sum_{k=1}^{n} k f_k g_{n-k}."""
    if f[0] != 0:
        raise ValueError("constant term must be 0")
    g = [Fraction(1)] + [Fraction(0)] * (len(f) - 1)
    for n in range(1, len(f)):
        g[n] = sum(k * f[k] * g[n - k] for k in range(1, n + 1)) / n
    return g

def exp_series(c=1):
    """e^(c x) as a truncated series."""
    return [Fraction(c) ** n / math.factorial(n) for n in range(N + 1)]

def egf_counts(series):
    """Turn EGF coefficients into counts a_n = n! [x^n]."""
    counts = [s * math.factorial(n) for n, s in enumerate(series)]
    assert all(c.denominator == 1 for c in counts)
    return [int(c) for c in counts]

def set_partitions_egf():
    e = exp_series()
    return egf_counts(series_exp([Fraction(0)] + e[1:]))

def involutions_egf():
    f = [Fraction(0)] * (N + 1)
    f[1], f[2] = Fraction(1), Fraction(1, 2)
    return egf_counts(series_exp(f))

def derangements_egf():
    geometric = [Fraction(1)] * (N + 1)
    return egf_counts(series_mul(exp_series(-1), geometric))

def count_set_partitions(n):
    """Brute force: build partitions of {0..n-1} element by element."""
    blocks_list = [[]]
    for x in range(n):
        new = []
        for blocks in blocks_list:
            for i in range(len(blocks)):
                new.append(blocks[:i] + [blocks[i] + [x]] + blocks[i + 1:])
            new.append(blocks + [[x]])
        blocks_list = new
    return len(blocks_list)

def count_involutions(n):
    return sum(1 for p in itertools.permutations(range(n))
               if all(p[p[i]] == i for i in range(n)))

def count_derangements(n):
    return sum(1 for p in itertools.permutations(range(n))
               if all(p[i] != i for i in range(n)))

def main():
    rows = [
        ("set partitions (Bell)", "exp(e^x - 1)", set_partitions_egf(),
         count_set_partitions),
        ("involutions", "exp(x + x^2/2)", involutions_egf(), count_involutions),
        ("derangements", "e^(-x)/(1 - x)", derangements_egf(),
         count_derangements),
    ]
    for name, egf, coeffs, brute in rows:
        checked = all(coeffs[n] == brute(n) for n in range(8))
        print(f"{name} from {egf}:")
        print(f"  n = 0..10: {coeffs[:11]}")
        print(f"  equal to brute-force counts for n <= 7: {checked}")
    print()
    d = derangements_egf()
    print("Derangements: D(n)/n! approaches 1/e = 0.3678794412...")
    for n in (3, 6, 9, 12):
        ratio = Fraction(d[n], math.factorial(n))
        error = abs(float(ratio) - math.exp(-1))
        bound = 1 / math.factorial(n + 1)
        print(f"  n = {n:>2}: D(n)/n! = {float(ratio):.10f}, error {error:.2e}"
              f" <= {bound:.2e}: {error <= bound}")
    print(f"  D(n) = round(n!/e) for 1 <= n <= 12: "
          f"{all(d[n] == round(math.factorial(n) / math.e) for n in range(1, 13))}")
    print()
    print("Epistemic status: exact rational arithmetic for the series and "
          "exhaustive\nenumeration for n <= 7, so the agreement is a finite "
          "check of the\nexponential formula, not a proof; the error column "
          "uses floating point,\nwhich is ample at this precision.")

if __name__ == "__main__":
    main()
Runs entirely in your browser, in an isolated worker. Nothing is sent to a server.

Used in