Three counting puzzles
Secret gifts. Twelve colleagues in an office in Accra put their names in a bowl, and each draws one to choose a recipient for a secret gift. What is the chance that nobody draws their own name? The answer is — and it barely changes whether there are twelve people or twelve thousand. That number is .
Ways to make change. In how many ways can be written as a sum of positive integers, ignoring order? For there are five: , , , , . For there are . No simple closed formula exists, yet these numbers obey astonishing identities: Euler noticed in 1748 that writing as a sum of odd parts and as a sum of distinct parts can always be done in the same number of ways.
Beadwork. An artisan strings six beads, each red or white, into a necklace. Two necklaces are the same if one can be rotated into the other. How many different necklaces are there? Not , which is not even an integer: the honest answer, , requires thinking about symmetry.
All three are problems of enumerative combinatorics, and the main tools for all three are generating functions — power series whose coefficients are the numbers we want — and group actions. This lesson builds on recurrences and generating functions, combinatorics (binomial coefficients, inclusion–exclusion) and groups (group actions and the orbit–stabiliser theorem).
A clothesline for numbers
Herbert Wilf described a generating function as "a clothesline on which we hang up a sequence of numbers for display". The point is that operations on sequences that are awkward to describe become simple operations on power series. If counts ways to build something of size from one kind of part and from another, then the number of ways to build something of size from one of each is — exactly the coefficient of in the product .
Two flavours matter. When the pieces are unlabelled — coins, parts of a partition — we use the ordinary generating function . When the pieces carry labels — people, positions, elements of a set — combining two structures also involves choosing which labels go to which, a factor . The exponential generating function absorbs those binomial coefficients, so that labelled products again become products of series.
Symmetry is the third idea. Counting necklaces means counting orbits of the rotation group acting on colourings. Dividing the number of colourings by the size of the group would be right if every orbit had the same size — but symmetric colourings have small orbits. Burnside's lemma counts orbits correctly by averaging the number of colourings each symmetry fixes.
Definitions
Definition— ordinary and exponential generating functions
The ordinary generating function (OGF) of a sequence is the formal power series ; its exponential generating function (EGF) is . We write for the coefficient of .
"Formal" means that is a symbol, not a number: we add and multiply series coefficientwise as polynomials, and never ask whether they converge. In the ring of formal power series, a series with nonzero constant term has an inverse — for instance — and infinite sums and products make sense as long as each coefficient is affected by only finitely many terms. For example is a well-defined series, because the factor cannot affect coefficients below .
Definition— partition
A partition of is a way of writing as an unordered sum of positive integers, its parts; we list the parts in non-increasing order, with . The number of partitions of is , with (the empty partition).
Definition— derangement, set partition, Stirling and Bell numbers
A derangement of a set is a permutation with no fixed point; is the number of derangements of an -element set. A set partition of a set is a collection of nonempty, pairwise disjoint blocks whose union is . The Stirling number of the second kind counts set partitions of into blocks, and the Bell number counts all of them. The Stirling number of the first kind counts permutations of with exactly cycles.
Definition— group action, orbit, stabiliser, fixed set
A group acts on a set if each gives a map with and . The orbit of is , its stabiliser is , and the fixed set of is . The orbits partition ; denotes the set of orbits.
Partitions and ordinary generating functions
Theorem— Theorem 1 (Euler)
More generally, if is any set of positive integers, the partitions of with all parts in are counted by , and those with distinct parts in by .
Proof
Expand each factor as a geometric series: , where will be the number of parts equal to . Multiplying out, a term arises once for each sequence of non-negative integers, all but finitely many zero, with . Such sequences are exactly the partitions of ( copies of ). For distinct parts each is or , giving the factor . Only the factors with affect , so all the products are well defined.
∎
Theorem— Theorem 2 (Euler 1748)
For every , the number of partitions of into odd parts equals the number of partitions of into distinct parts.
Proof
By generating functions. By Theorem 1 we must show . Since ,
because the even factors with cancel between numerator and denominator. The remaining numerator is plus terms of degree greater than . So the two sides of the claimed identity have the same coefficients up to , for every .
By a bijection (Glaisher 1883). Given a partition into odd parts in which the odd number occurs times, write in binary, with distinct , and replace the copies of by the parts . These parts are distinct: two of them, and with odd, are equal only if and , since every positive integer is uniquely a power of times an odd number. Conversely, from a partition into distinct parts, split each part ( odd) into copies of . The two maps are inverse to each other, because binary expansions are unique.
∎
Example— Glaisher's bijection for n = 8
(two s merge into a , two s into a ); (five s: ); . All six partitions of into odd parts, and their images, are listed by the experiment below.
Example— Conjugation and Ferrers diagrams
Draw a partition as left-justified rows of dots, row having dots (its Ferrers diagram). Reflecting in the diagonal gives the conjugate partition , whose parts are the column lengths. Conjugation is a bijection, and it proves at a glance that the number of partitions of into at most parts equals the number into parts of size at most .
Partitions have no simple closed formula, but a great deal is known about them — all stated here without proof:
- The pentagonal number theorem (Euler): . Multiplying by gives the fast recurrence .
- Hardy and Ramanujan (1918): as , proved with the circle method; Rademacher (1937) turned their series into an exact convergent formula.
- Ramanujan's congruences: , and , found by Ramanujan, who proved the first two in 1919.
Labelled structures and exponential generating functions
Theorem— Theorem 3 (the product rule for EGFs)
Let and count two kinds of structures on an -element set of labels. The number of ways to split an -element set into two parts and put a structure of the first kind on and of the second on is , and .
Proof
There are choices of with , then and choices of structures. And .
∎
Theorem— Theorem 4 (the exponential formula)
Let count "connected" structures on an -element set, with , and let count the ways to partition an -element set into blocks and put a connected structure on each block (). Then
Proof
By Theorem 3 applied times, counts ordered sequences of connected structures on blocks that partition the label set. The blocks are nonempty (since ) and disjoint, hence distinct, so each unordered family of blocks arises from exactly orderings. Thus is the EGF of structures with exactly blocks, and summing over gives the formula. (The sum makes sense in because has no terms below .)
∎
Example— Four applications of the exponential formula
- Set partitions. A block carries exactly one (trivial) structure on any nonempty set: . So , and .
- Permutations. A permutation is a set of disjoint cycles, and there are cyclic orders on a -set, so . Then : there are permutations, as there should be.
- Derangements are permutations with no 1-cycles: , so .
- Involutions () have cycles of length and only: .
Theorem— Theorem 5 (derangements)
Consequently is the integer nearest to for , and the probability that a uniformly random permutation is a derangement tends to .
Proof
By the EGF. , and multiplying by forms partial sums of coefficients: . Multiply by .
By inclusion–exclusion. Let be the set of permutations of fixing . A permutation fixing prescribed points is a permutation of the other , so , and
For the consequences: , an alternating series with decreasing terms, so its absolute value is at most the first term, for . Dividing by , .
∎
Example— A recurrence, proved by a bijection
for . In a derangement of , let : there are choices. If , then swaps and and deranges the other points: possibilities. Otherwise let , and define on by and elsewhere. Then is a derangement (since ), and is a bijection onto all derangements of points: possibilities. So — and for twelve colleagues, .
Theorem— Theorem 6 (cycles of permutations)
In particular the expected number of cycles of a uniformly random permutation of points is .
Proof
Build a permutation of from one of by inserting : either as a new fixed point (one new cycle, a factor ), or immediately after one of the existing elements in its cycle ( ways, no new cycle). Every permutation of arises exactly once, so with . For the expectation, differentiate: .
∎
The second-kind numbers also have a clean interpretation: a function from a -set to an -set is determined by the partition of its domain into nonempty fibres and an injective labelling of the fibres, so .
Counting up to symmetry
Theorem— Theorem 7 (Burnside's lemma, also called the Cauchy–Frobenius lemma)
If a finite group acts on a finite set , the number of orbits is the average number of fixed points:
Proof
Count the set in two ways. Grouping by gives ; grouping by gives . By the orbit–stabiliser theorem , so
∎
Theorem— Corollary 8 (necklaces)
The number of necklaces of beads in colours, up to rotation, is
where is Euler's totient function.
Proof
The cyclic group of rotations acts on the colourings of the positions . The rotation moves position to ; its cycles on positions are the cosets of the subgroup generated by , which has elements, so has cycles. A colouring is fixed by exactly when it is constant on each cycle, so . Finally, for exactly values of (write with and ). Burnside's lemma completes the proof.
∎
For and : the six rotations fix colourings, so there are necklaces. The naive count fails because, for instance, the all-red colouring is alone in its orbit.
Pólya's refinement. The proof of Corollary 8 used only the number of cycles of each symmetry. For a group of permutations of positions, let be the number of cycles of length of , and define the cycle index
Then the number of colourings with colours up to symmetry is , and — Pólya's enumeration theorem (Redfield 1927, Pólya 1937) — substituting (one variable per colour) gives a polynomial whose coefficient of is the number of inequivalent colourings using each colour the prescribed number of times. (Proof: apply Burnside's lemma to the -invariant set of colourings with given colour counts; a colouring fixed by is constant on the cycles of , and a cycle of length contributes or or ….)
Example— Colouring the faces of a cube
The 24 rotations of a cube permute its 6 faces: the identity (); 6 quarter turns about face axes (); 3 half turns about face axes (); 8 turns by about vertex axes (); 6 half turns about edge axes (). So
and with colours there are inequivalent cubes: for two colours, for three.
How many cubes have three red and three white faces? Substitute and extract the coefficient of term by term: gives ; gives (from ); gives ; and and contain no . So there are such cubes: the three red faces either meet at a corner, or two of them are opposite and the third joins them.
A useful check on such computations: Burnside's lemma always produces an integer, so a fractional answer signals a missed term.
Counting in Lean and Mathlib
Mathlib has a good deal of enumerative combinatorics. Derangements are a finite type with a recursively computed count; the library proves that the recursion counts them and that . The closed formula of Theorem 5 is proved here by induction from Mathlib's recurrence :
/-- The number of fixed-point-free permutations of a finite set depends only on its size, and
is computed by Mathlib's recursively defined `numDerangements`, which satisfies
`D(n + 2) = (n + 1) (D(n) + D(n + 1))`. This applies `card_derangements_eq_numDerangements`. -/
theorem card_derangements (α : Type*) [Fintype α] [DecidableEq α] :
Fintype.card (derangements α) = numDerangements (Fintype.card α) :=
card_derangements_eq_numDerangements α/-- **The derangement formula** `D(n) = n! Σ_{k=0}^{n} (-1)^k / k!` (the inclusion–exclusion
formula of the lesson), proved by induction from the recurrence `D(n + 1) = (n + 1) D(n) + (-1)^(n+1)`
(Mathlib's `numDerangements_succ`): multiplying the formula for `n` by `n + 1` gives
`(n + 1)! Σ_{k ≤ n}`, and the new term is `(n + 1)! (-1)^(n+1) / (n + 1)! = (-1)^(n+1)`. -/
theorem derangements_formula (n : ℕ) :
(numDerangements n : ℚ) =
n.factorial * ∑ k ∈ range (n + 1), (-1 : ℚ) ^ k / k.factorial := by
induction n with
| zero => simp [numDerangements_zero]
| succ n ih =>
have h : (numDerangements (n + 1) : ℚ) = (n + 1) * (numDerangements n : ℚ) - (-1) ^ n := by
exact_mod_cast numDerangements_succ n
rw [h, ih, sum_range_succ (fun k => (-1 : ℚ) ^ k / k.factorial) (n + 1),
Nat.factorial_succ]
push_cast
have hfact : (n.factorial : ℚ) ≠ 0 := by exact_mod_cast Nat.factorial_ne_zero n
field_simp
ring/-- **A random permutation is a derangement with probability tending to `1/e`.** This applies
Mathlib's `numDerangements_tendsto_inv_e`. -/
theorem derangements_tendsto :
Filter.Tendsto (fun n => (numDerangements n : ℝ) / n.factorial) Filter.atTop
(nhds (Real.exp (-1))) :=
numDerangements_tendsto_inv_eEuler's partition theorem is in Mathlib, proved — like our first proof — by identifying generating functions, as the case of Glaisher's theorem; and the Stirling identity at the end of the previous section:
/-- **Euler's partition theorem** (1748): the partitions of `n` into odd parts are as numerous as
the partitions of `n` into distinct parts. Mathlib proves it, as in the lesson, by showing that
the two generating functions `Π 1/(1 - x^(2k-1))` and `Π (1 + x^k)` are equal (as the case
`m = 2` of Glaisher's theorem). This applies `Nat.Partition.card_odds_eq_card_distincts`. -/
theorem euler_partition (n : ℕ) :
(Nat.Partition.odds n).card = (Nat.Partition.distincts n).card :=
Nat.Partition.card_odds_eq_card_distincts n/-- **Counting functions by their image**: `m^k = Σ_j S(k, j) · m(m-1)⋯(m-j+1)`, where the
Stirling number of the second kind `S(k, j)` counts partitions of a `k`-set into `j` blocks: a
function from a `k`-set to an `m`-set is a partition of its domain into `j` nonempty fibres
together with an injective labelling of the fibres. This applies Mathlib's
`Nat.pow_eq_sum_stirlingSecond_mul_descFactorial`. -/
theorem pow_eq_sum_stirling (m k : ℕ) :
m ^ k = ∑ j ∈ range (k + 1), Nat.stirlingSecond k j * m.descFactorial j :=
Nat.pow_eq_sum_stirlingSecond_mul_descFactorial m kBurnside's lemma. The double-counting step of the proof is proved here; orbit–stabiliser and Burnside's lemma itself are quoted from Mathlib, which states the lemma without division, as :
/-- **The double-counting step of Burnside's lemma.** Count the pairs `(g, x)` with `g • x = x`
in two ways: by `g` (the fixed points of each group element) or by `x` (the stabiliser of each
point). Both sums are the size of the same set. -/
theorem burnside_double_count {G X : Type*} [Group G] [MulAction G X] [Fintype G] [Fintype X]
[DecidableEq X] :
∑ g : G, #(univ.filter fun x : X => g • x = x) =
∑ x : X, #(univ.filter fun g : G => g • x = x) := by
simp only [card_filter]
exact sum_comm/-- **The orbit–stabiliser theorem**: `|orbit x| · |Stab x| = |G|`. Dividing the stabiliser sum
above by `|G|` therefore gives `Σ_x 1/|orbit x|`, which is the number of orbits, since each orbit
contributes `1`. This applies Mathlib's `MulAction.card_orbit_mul_card_stabilizer_eq_card_group`. -/
theorem orbit_stabilizer {G X : Type*} [Group G] [MulAction G X] [Fintype G] (x : X)
[Fintype (MulAction.orbit G x)] [Fintype (MulAction.stabilizer G x)] :
Fintype.card (MulAction.orbit G x) * Fintype.card (MulAction.stabilizer G x) =
Fintype.card G :=
MulAction.card_orbit_mul_card_stabilizer_eq_card_group G x/-- **Burnside's lemma** (Cauchy–Frobenius): the number of orbits times `|G|` is the total number
of fixed points, `Σ_g |Fix g|`. This applies Mathlib's
`MulAction.sum_card_fixedBy_eq_card_orbits_mul_card_group`. -/
theorem burnside {G X : Type*} [Group G] [MulAction G X] [Fintype G]
[∀ g : G, Fintype (MulAction.fixedBy X g)] [Fintype (MulAction.orbitRel.Quotient G X)] :
∑ g : G, Fintype.card (MulAction.fixedBy X g) =
Fintype.card (MulAction.orbitRel.Quotient G X) * Fintype.card G :=
MulAction.sum_card_fixedBy_eq_card_orbits_mul_card_group G XNecklaces, checked by computation. Encode a 6-bead necklace as a number below ; a rotation class has a unique smallest member. Counting these canonical members gives , and counting fixed colourings of each rotation gives the numbers used in Corollary 8:
/-- A colouring is the *canonical representative* of its rotation class if it is the smallest
number among its rotations. Each necklace has exactly one canonical representative. -/
def isCanonical (n x : ℕ) : Bool :=
(List.range n).all fun k => x ≤ rotate n k x/-- **There are 14 binary necklaces with 6 beads** (up to rotation): exactly 14 of the 64
colourings are canonical. Checked by `decide`. -/
theorem necklaces_six : ((List.range 64).filter (isCanonical 6)).length = 14 := by
decide/-- **The fixed-point counts in Burnside's lemma for necklaces.** Rotation by `k` places fixes
exactly `2^gcd(k, 6)` of the 64 colourings (a fixed colouring is periodic with period
`gcd(k, 6)`). So Burnside's lemma gives `(64 + 2 + 4 + 8 + 4 + 2) / 6 = 84 / 6 = 14` orbits,
agreeing with `necklaces_six`. Checked by `decide`. -/
theorem necklace_fixed_counts :
(∀ k < 6, ((List.range 64).filter fun x => rotate 6 k x = x).length = 2 ^ Nat.gcd k 6) ∧
(∑ k ∈ range 6, 2 ^ Nat.gcd k 6) / 6 = 14 := by
decideWhat is not formalised here: Glaisher's bijection, the exponential formula, the cycle-index form of Pólya's theorem, and the general necklace formula (only the case is checked).
Generating functions, computed and checked
The first program expands the generating functions of Theorem 1 exactly — multiplying by is the update — and checks Euler's theorem up to , runs Glaisher's bijection on all partitions of , and compares with the Hardy–Ramanujan formula.
"""Integer partitions: generating functions, Euler's theorem and Glaisher's map.
We expand the generating functions
prod 1/(1 - x^k) (all partitions, p(n)),
prod 1/(1 - x^(2k-1)) (partitions into odd parts),
prod (1 + x^k) (partitions into distinct parts)
as exact integer power series up to x^60, and compare the last two, as
Euler's theorem predicts. For n = 8 we also list Glaisher's bijection, which
merges equal odd parts into distinct parts, and check that it is a bijection.
Finally we compare p(n) with the Hardy-Ramanujan asymptotic formula and test
Ramanujan's congruence p(5m + 4) = 0 (mod 5) on a range.
All counts are exact. Agreement up to x^60 is evidence; Euler's theorem is
proved for all n in the lesson (and in Mathlib).
"""
import math
def times_geometric(series, step):
"""Multiply a truncated power series by 1/(1 - x^step), in place.
If b = a / (1 - x^step) then b = a + x^step * b, so b[i] = a[i] + b[i - step].
"""
for i in range(step, len(series)):
series[i] += series[i - step]
return series
def times_one_plus(series, step):
"""Multiply a truncated power series by (1 + x^step), in place."""
for i in range(len(series) - 1, step - 1, -1):
series[i] += series[i - step]
return series
def partition_counts(n_max):
"""p(0), ..., p(n_max): coefficients of the product of 1/(1 - x^k)."""
series = [1] + [0] * n_max
for k in range(1, n_max + 1):
times_geometric(series, k)
return series
def odd_part_counts(n_max):
"""Coefficients of the product of 1/(1 - x^k) over odd k."""
series = [1] + [0] * n_max
for k in range(1, n_max + 1, 2):
times_geometric(series, k)
return series
def distinct_part_counts(n_max):
"""Coefficients of the product of (1 + x^k)."""
series = [1] + [0] * n_max
for k in range(1, n_max + 1):
times_one_plus(series, k)
return series
def partitions(n, largest=None):
"""All partitions of n as non-increasing tuples."""
if largest is None:
largest = n
if n == 0:
return [()]
out = []
for first in range(min(n, largest), 0, -1):
out += [(first,) + rest for rest in partitions(n - first, first)]
return out
def glaisher(odd_partition):
"""Merge equal odd parts: m copies of k become k * 2^i for the bits of m."""
parts = []
for k in set(odd_partition):
m = odd_partition.count(k)
i = 0
while m:
if m & 1:
parts.append(k * 2 ** i)
m >>= 1
i += 1
return tuple(sorted(parts, reverse=True))
def hardy_ramanujan(n):
"""The leading asymptotic exp(pi sqrt(2n/3)) / (4 n sqrt 3)."""
return math.exp(math.pi * math.sqrt(2 * n / 3)) / (4 * n * math.sqrt(3))
def main():
n_max = 60
odd = odd_part_counts(n_max)
distinct = distinct_part_counts(n_max)
print("Generating functions expanded exactly up to x^60:")
print(f" odd-part counts: {odd[:13]} ...")
print(f" distinct-part counts: {distinct[:13]} ...")
print(f" equal for every n <= {n_max}: {odd == distinct}")
print()
eights = partitions(8)
odds8 = [q for q in eights if all(part % 2 for part in q)]
images = [glaisher(q) for q in odds8]
print(f"Glaisher's map on the {len(odds8)} odd-part partitions of 8:")
for q, image in zip(odds8, images):
print(f" {'+'.join(map(str, q)):<17} -> {'+'.join(map(str, image))}")
distinct8 = {q for q in eights if len(set(q)) == len(q)}
print(f" images distinct and exactly the distinct-part partitions: "
f"{len(set(images)) == len(images) and set(images) == distinct8}")
print()
p = partition_counts(300)
print("p(n) versus Hardy-Ramanujan exp(pi sqrt(2n/3))/(4n sqrt 3):")
for n in (10, 50, 100, 300):
print(f" n = {n:>3}: p(n) = {p[n]:<19} ratio = "
f"{p[n] / hardy_ramanujan(n):.4f}")
bad = [n for n in range(4, 300, 5) if p[n] % 5]
print(f"Ramanujan: p(5m + 4) divisible by 5 for all 5m + 4 < 300: {not bad}")
print()
print("Epistemic status: exact integer computation, exhaustive for n <= 60 "
"(Euler)\nand n < 300 (Ramanujan), and for n = 8 (Glaisher). These "
"are finite checks\nof theorems proved in general; the asymptotic "
"ratios are numerical and only\nillustrate that the relative error "
"tends to 0, slowly.")
if __name__ == "__main__":
main()The second computes exponential generating functions with exact fractions, using for a series with no constant term via , and compares their coefficients with brute-force counts.
"""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()The third counts necklaces, bracelets and coloured cubes twice — by listing colourings and keeping the smallest in each orbit, and by Burnside's lemma — and shows how far off the naive count is.
"""Counting up to symmetry: necklaces, bracelets and coloured cubes.
Two colourings are "the same" if a symmetry carries one to the other, so we
are counting orbits of a group acting on colourings. We count orbits in two
independent ways and compare:
* brute force: list all colourings and pick one canonical representative
(the smallest) from each orbit;
* Burnside's lemma: average the number of colourings fixed by each group
element, which for necklaces gives (1/n) sum_{d | n} phi(d) k^(n/d), and
for the cube is Polya's cycle-index polynomial evaluated at k colours.
We also show why the naive count k^n / n is wrong: symmetric colourings
have smaller orbits. The checks are exhaustive for the sizes shown.
"""
import itertools
from fractions import Fraction
from math import gcd
def phi(n):
"""Euler's totient function."""
return sum(1 for a in range(1, n + 1) if gcd(a, n) == 1)
def rotations(n):
"""The n rotations of an n-bead necklace, as position permutations."""
return [tuple((i + r) % n for i in range(n)) for r in range(n)]
def dihedral(n):
"""Rotations and reflections: the dihedral group of order 2n."""
return rotations(n) + [tuple((r - i) % n for i in range(n)) for r in range(n)]
def act(perm, colouring):
"""The colouring moved by a permutation of positions."""
return tuple(colouring[perm[i]] for i in range(len(perm)))
def orbits_brute_force(group, k, size):
"""Number of orbits: count colourings that are smallest in their orbit."""
return sum(1 for c in itertools.product(range(k), repeat=size)
if all(c <= act(g, c) for g in group))
def cycles(perm):
"""Number of cycles of a permutation."""
seen, count = set(), 0
for start in range(len(perm)):
if start not in seen:
count += 1
i = start
while i not in seen:
seen.add(i)
i = perm[i]
return count
def orbits_burnside(group, k):
"""Burnside: average of |Fix g|; a permutation with c cycles fixes k^c."""
total = sum(k ** cycles(g) for g in group)
return Fraction(total, len(group))
def necklace_formula(n, k):
"""(1/n) sum over divisors d of n of phi(d) k^(n/d)."""
return Fraction(sum(phi(d) * k ** (n // d) for d in range(1, n + 1)
if n % d == 0), n)
def cube_rotations():
"""The 24 rotations of a cube acting on its faces U, D, F, B, L, R."""
spin = (0, 1, 5, 4, 2, 3) # quarter turn about the U-D axis
tilt = (2, 3, 1, 0, 4, 5) # quarter turn about the L-R axis
group, frontier = {tuple(range(6))}, [tuple(range(6))]
while frontier:
g = frontier.pop()
for h in (spin, tilt):
gh = tuple(g[h[i]] for i in range(6))
if gh not in group:
group.add(gh)
frontier.append(gh)
return sorted(group)
def main():
print("Necklaces of n beads in k colours (rotations only):")
print(" n k brute force Burnside formula naive k^n/n")
for n, k in [(4, 2), (6, 2), (6, 3), (8, 2), (9, 2), (9, 3)]:
group = rotations(n)
brute = orbits_brute_force(group, k, n)
burn = orbits_burnside(group, k)
naive = Fraction(k ** n, n)
print(f"{n:>4} {k:>2} {brute:>13} {str(burn):>10} "
f"{str(necklace_formula(n, k)):>9} {float(naive):>13.2f}")
print("Bracelets (rotations and reflections), k = 2:")
for n in (4, 6, 8, 9):
group = dihedral(n)
print(f" n = {n}: brute force {orbits_brute_force(group, 2, n)}, "
f"Burnside {orbits_burnside(group, 2)}")
print()
cube = cube_rotations()
types = sorted(cycles(g) for g in cube)
print(f"Cube: {len(cube)} rotations generated from two quarter turns.")
print(f" number with c cycles on the faces, c = 1..6: "
f"{[types.count(c) for c in range(1, 7)]}")
for k in (2, 3):
print(f" face colourings with {k} colours up to rotation: brute force "
f"{orbits_brute_force(cube, k, 6)}, Burnside "
f"{orbits_burnside(cube, k)}")
print()
print("Epistemic status: exact and exhaustive for every size shown - each "
"count\nwas obtained by listing all colourings and agrees with "
"Burnside's lemma.\nThat is evidence for the formulas at these sizes; "
"the lemma itself is\nproved in the lesson (and in Mathlib).")
if __name__ == "__main__":
main()All three are exact and exhaustive at the sizes shown: strong evidence for the formulas, and a check on our implementations, but not proofs for all .
Explore: the derangement probability
is the -th partial sum of the series . Watch the partial sums home in on :
Partial sums of Σ (−1)^k / k! → 1/e
The n-th partial sum is D(n−1)/(n−1)!, the chance that a random permutation of n − 1 objects has no fixed point.
- partial sums Sₙ
- terms aₙ
- Known sum
S12 = a1 + … + a12 = 0.367879
Known sum: L = exp(−1) ≈ 0.367879 · still missing: L − S12 ≈ 1.94×10⁻⁹
Partial sums on a screen are evidence, never proof: a series can look settled and still diverge, as the harmonic series does.
Things to notice: the partial sums alternate above and below , and the error after terms is smaller than the next term — which is why the probability is practically constant from about seven people onwards. Change the term to 1/(n-1)! to see itself appear: the EGF of the sequence is .
Exercises
1. Enumerative combinatorics
Warm-upHumanHow many derangements (permutations with no fixed point) does a set of elements have?
2. Enumerative combinatorics
Warm-upHumanHow many partitions of into distinct parts are there?
3. Enumerative combinatorics
CoreHumanHow many necklaces of beads, each bead one of colours, are there up to rotation?
4. Enumerative combinatorics
CoreHumanA student counts two-colour necklaces with beads up to rotation as . The true number is . What exactly goes wrong?
5. Enumerative combinatorics
CoreHumanGlaisher's bijection merges equal odd parts: copies of an odd number become the parts for the powers in the binary expansion of . What is the image of the partition of ?
6. Enumerative combinatorics
StretchHumanWhich exponential generating function counts the permutations of all of whose cycles have odd length?
7. Enumerative combinatorics
CoreHumanIn the proof of Burnside's lemma we used . Which fact justifies this step?
8. Enumerative combinatorics
CoreFormalMathlib writes fixedBy X g for the fixed set of g, stabilizer G x for the stabiliser and orbitRel.Quotient G X for the set of orbits. Which statement is Burnside's lemma (true for every finite group acting on a finite set)?
9. Enumerative combinatorics
StretchComputationalWrite necklaces(n, k) returning the number of necklaces of beads in colours up to rotation, using Corollary 8: . Write your own totient function phi(d), and return an int.
Common misconceptions
Misconception
"To count up to symmetry, divide by the number of symmetries." That works only when every orbit has full size , i.e. when no colouring is fixed by a non-identity symmetry. For two-colour necklaces of four beads, , but the true count is : the colourings RRRR, WWWW (fixed by every rotation) and RWRW (fixed by the half-turn) have small orbits. Burnside's lemma corrects for exactly this.
Misconception
"A generating function must converge to be useful." Formal power series are algebraic objects; identities between them are identities of coefficients, valid whether or not the series converge. converges only at , yet it is a perfectly good element of . Convergence matters only when one wants analytic information, such as asymptotics.
Misconception
"Ordinary and exponential generating functions are interchangeable." They encode different combinatorics. OGFs multiply for unlabelled combinations (the coefficient of a product is ); EGFs multiply for labelled ones (with built in). Using an OGF for permutations or set partitions gives the wrong convolution.
Misconception
"The probability of no fixed point goes to zero for large groups, since there are more people to collide with." More people means more chances for someone to draw their own name, but each chance is smaller; the two effects balance, and the probability tends to , not to .
Many ways of seeing
Algebra— Partitions and the symmetric group
The partitions of index the conjugacy classes of the symmetric group (by cycle type) — and therefore, since the number of irreducible complex representations of a finite group equals its number of conjugacy classes, also its irreducible representations. Unusually, there is a natural bijection: the partition gives the Specht module , whose dimension is the number of standard Young tableaux of shape . The identity , true for any finite group's representations, has a bijective proof — the Robinson–Schensted correspondence between permutations and pairs of tableaux of the same shape. The cycle index of is the generating function of conjugacy classes, and Pólya theory grows into the theory of symmetric functions. See representation theory and groups.
Analysis— Singularities decide asymptotics
Treated as analytic functions, generating functions reveal how fast their coefficients grow: the coefficients are governed by the singularities nearest the origin. has a simple pole at with residue , so is entire and its coefficients decay faster than any exponential: for every , a much stronger statement than Theorem 5. For partitions, has the whole unit circle as a natural boundary; Hardy and Ramanujan's circle method analyses its behaviour near every root of unity, with the dominant contribution from , giving . Flajolet and Sedgewick's Analytic Combinatorics systematises this "singularity analysis". See complex analysis.
Number theory— Modular forms behind partitions
The partition generating function is essentially a modular form. With , the Dedekind eta function satisfies , so transforms predictably under — the source of the Hardy–Ramanujan–Rademacher formula. Euler's pentagonal number theorem is the -expansion of , a special case of Jacobi's triple product identity. Ramanujan's congruences modulo come from modular identities such as ; Dyson's rank (1944) and Andrews and Garvan's crank (1988) explain them combinatorially; and Ken Ono (2000) showed that congruences of this kind exist modulo every prime , though on more complicated arithmetic progressions. See analytic number theory.
Probability— Random permutations
The exponential formula is a probabilistic statement in disguise. For a uniformly random permutation of points, the number of fixed points converges in distribution to a Poisson variable with mean : . More generally the numbers of cycles of lengths converge jointly to independent Poisson variables with means , reflecting the factorisation . The total number of cycles has mean (Theorem 6) and is asymptotically normal (Goncharov, 1944). The longest cycle typically covers a positive fraction of all points (about on average — the Golomb–Dickman constant), a fact with a surprising twin in the distribution of the largest prime factor of a random integer. See probability theory.
Geometry— Symmetry groups of solids and molecules
Burnside's lemma is geometry made countable: the group in the cube example is the rotation group of the cube, isomorphic to acting on the four long diagonals, and the cycle types were read off from its axes of symmetry. Pólya's original motivation (1937) was chemistry: counting isomers, molecules with the same formula but different arrangements, such as the substituted benzenes, where the group is the dihedral symmetry group of the hexagonal ring. Replacing the cube by the icosahedron, or the necklace by a crystal lattice, changes only the group and its cycle index. The naive count has a geometric meaning too. Weighting each orbit by instead of gives , the cardinality of the quotient regarded as an orbifold (or groupoid), in which a point with symmetry counts only fractionally; for a finite group acting on a finite complex, the orbifold Euler characteristic is likewise . Burnside's lemma is what you need when every orbit must count as exactly one. See symmetry and transformations.
Category theory— Species: generating functions as functors
André Joyal's theory of combinatorial species (1981) explains why EGFs work. A species is a functor from the category of finite sets and bijections to itself: it sends a label set to the set of structures on (all set partitions of , all permutations of , …) and relabels structures along bijections. Its EGF is . Operations on species — sum, product (split the label set), composition (partition the labels, put a structure on each block) — correspond to sum, product and composition of EGFs, so Theorem 3 and the exponential formula become functorial statements: the species of sets has EGF , and "sets of connected structures" is the composite , with EGF . Counting unlabelled structures uses the cycle index of a species, which unifies Pólya theory with EGFs. See functors.
History and global context
- Early combinatorics in India. Piṅgala's Chandaḥśāstra (last centuries BCE) on Sanskrit prosody counted the patterns of short and long syllables, which leads to binary sequences and to the numbers now called binomial coefficients (the meru-prastāra); Virahāṅka and later Hemacandra (c. 1150) counted metres of a given total length by what are now called Fibonacci numbers. Bhāskara II's Līlāvatī (1150) and Nārāyaṇa Paṇḍita's Gaṇita Kaumudī (1356) treat permutations and combinations systematically.
- The Islamic world, China and Europe. Al-Karajī (c. 1000) worked with the binomial coefficients; in Marrakesh in the 13th century Ibn Munʿim counted the words that can be formed from an alphabet and used the arithmetic triangle to do so. In China the triangle appears in Yang Hui's 1261 book, citing Jia Xian (11th century). Levi ben Gershon (1321) proved the formulas for permutations and combinations by induction.
- Probability and derangements. Pierre Rémond de Montmort posed the problème des rencontres — how often does no card fall in its own place? — in 1708, and solved it with Nicolaus Bernoulli; the limit emerged from their work and Euler's.
- Generating functions. Abraham de Moivre used generating functions to solve recurrences (1730), and Leonhard Euler's Introductio in analysin infinitorum (1748) founded the theory of partitions with them, including the odd/distinct theorem and the pentagonal number theorem (proved 1750). J. W. L. Glaisher gave his bijection in 1883; Percy MacMahon's Combinatory Analysis (1915–16) summed up the classical theory. G. H. Hardy and Srinivasa Ramanujan's asymptotic formula (1918), and Ramanujan's congruences (1919), opened the analytic and arithmetic theory of partitions.
- Symmetry. The orbit-counting lemma appears in work of Augustin-Louis Cauchy (1845) and Georg Frobenius (1887); William Burnside's 1897 textbook made it famous, so his name stuck. J. Howard Redfield (1927) and George Pólya (1937) developed the cycle-index method, Pólya with applications to chemical isomers. Gian-Carlo Rota's 1960s programme and André Joyal's species (1981) gave the field its modern foundations.
Further reading
- Richard P. Stanley, Enumerative Combinatorics, Volume 1 (1986; 2nd edition 2012) and Volume 2 (1999) — the standard reference; generating functions, partitions and symmetric functions.
- Herbert S. Wilf, generatingfunctionology (1990; 3rd edition 2006) — a lively introduction to generating functions.
- Philippe Flajolet and Robert Sedgewick, Analytic Combinatorics (2009) — symbolic method, EGFs and singularity analysis.
- George E. Andrews, The Theory of Partitions (1976) — the classic text on partitions.
- George E. Andrews and Kimmo Eriksson, Integer Partitions (2004) — an accessible introduction.
- Peter J. Cameron, Combinatorics: Topics, Techniques, Algorithms (1994) — includes Burnside's lemma and Pólya theory.
Next steps in the Lumenary: representation theory of finite groups, where partitions classify the representations of ; the probabilistic method; and analytic number theory.