Source code for mathematicskit.combinatorics.systems.inclusion_exclusion

r"""The inclusion-exclusion principle, and its classic application to
counting derangements.

No scipy/numpy equivalent. See Graham, Knuth & Patashnik, *Concrete
Mathematics*, 2nd ed., Sec. 8.3, and Cormen et al., *Introduction to
Algorithms*, 3rd ed., Ch. C.4 (the hat-check/derangement problem).
"""

from __future__ import annotations

import itertools
from collections.abc import Sequence

__all__ = ["union_size_inclusion_exclusion", "derangement_count"]


[docs] def union_size_inclusion_exclusion(sets: Sequence[set]) -> int: r"""Exact size of the union of several (possibly overlapping) sets. :math:`\left|\bigcup_i A_i\right| = \sum_i |A_i| - \sum_{i<j}|A_i \cap A_j| + \sum_{i<j<k}|A_i \cap A_j \cap A_k| - \dots`, summed over every nonempty subset of the given sets, alternating sign by subset size. See Graham, Knuth & Patashnik, *Concrete Mathematics*, 2nd ed., Sec. 8.3, eq. (8.63). Parameters ---------- sets : sequence of set Returns ------- int Examples -------- >>> a = {1, 2, 3, 4} >>> b = {3, 4, 5, 6} >>> c = {4, 5, 6, 7} >>> union_size_inclusion_exclusion([a, b, c]) == len(a | b | c) True """ n = len(sets) total = 0 for r in range(1, n + 1): sign = 1 if r % 2 == 1 else -1 for indices in itertools.combinations(range(n), r): intersection = sets[indices[0]] for i in indices[1:]: intersection = intersection & sets[i] total += sign * len(intersection) return total
[docs] def derangement_count(n: int) -> int: r"""Number of derangements :math:`D_n`: permutations of ``n`` items with no fixed points. The classic inclusion-exclusion application (the "hat-check problem"): starting from all :math:`n!` permutations, subtract those fixing each point, add back those fixing each pair (double- subtracted), etc., giving :math:`D_n = n!\sum_{k=0}^n \dfrac{(-1)^k}{k!}`. See Graham, Knuth & Patashnik, *Concrete Mathematics*, 2nd ed., Sec. 8.3, and Cormen et al., *Introduction to Algorithms*, 3rd ed., Ch. C.4. Parameters ---------- n : int ``n >= 0``. Returns ------- int Examples -------- >>> derangement_count(0) 1 >>> derangement_count(1) 0 >>> derangement_count(4) 9 >>> import itertools >>> brute_force = sum(1 for p in itertools.permutations(range(4)) if all(p[i] != i for i in range(4))) >>> derangement_count(4) == brute_force True """ if n < 0: raise ValueError("n must be >= 0") # Equivalent to n! * sum_{k=0}^n (-1)^k/k!, but computed via the # exact-integer recurrence D_n = (n-1)*(D_{n-1} + D_{n-2}) to avoid # any floating-point contamination from the factorial-ratio sum. d = [1, 0] if n < 2: return d[n] for i in range(2, n + 1): d.append((i - 1) * (d[i - 1] + d[i - 2])) return d[n]