Combinatorics contents
The Inclusion-Exclusion Principle
Count the elements of a union by alternately adding and subtracting intersections; derangements, coprime counts and multiples in a range.
Read first: Binomial Coefficients, Bit Manipulation
The inclusion-exclusion principle counts how many elements belong to at least one of several sets without listing them. If you add , the elements in both sets are counted twice, so subtract . With three sets we must add the triple intersection back, and so on:
or, more compactly, summing over all non-empty subsets of the sets:
Why it works. An element that lies in exactly of the sets is counted time by the right-hand side, since .
The usual form: count what avoids all properties
Often we want the number of elements that have none of the properties . With elements in total and = elements with property :
(the empty subset contributes ). Enumerate the subsets with a bitmask. That costs terms, so it is used with up to about 20.
Example 1: derangements
A derangement is a permutation of elements with no fixed point ( for all ). Let = permutations with ; the intersection of any of them contains permutations, and there are ways to pick the sets:
from math import comb, factorial
from itertools import permutations
def derangements(n):
return sum((-1) ** k * comb(n, k) * factorial(n - k) for k in range(n + 1))
def derangements_brute(n):
return sum(all(p[i] != i for i in range(n)) for p in permutations(range(n)))
assert [derangements(n) for n in range(9)] == [1, 0, 1, 2, 9, 44, 265, 1854, 14833]
assert all(derangements(n) == derangements_brute(n) for n in range(8))
# also D_n = (n - 1) * (D_{n-1} + D_{n-2})
D = [1, 0]
for n in range(2, 12):
D.append((n - 1) * (D[-1] + D[-2]))
assert D == [derangements(n) for n in range(12)]The fraction tends to : a random permutation has no fixed point about 37% of the time.
Example 2: numbers in coprime to
is coprime to if it's divisible by none of the prime factors of . Let the distinct primes of be and = multiples of . The multiples of in number :
from math import gcd
def prime_factors(m):
ps, d = [], 2
while d * d <= m:
if m % d == 0:
ps.append(d)
while m % d == 0:
m //= d
d += 1
if m > 1:
ps.append(m)
return ps
def count_coprime(n, m):
primes = prime_factors(m)
total = 0
for mask in range(1 << len(primes)):
product, bits = 1, 0
for i, p in enumerate(primes):
if mask >> i & 1:
product *= p
bits += 1
total += (-1) ** bits * (n // product)
return total
assert count_coprime(30, 30) == 8 # phi(30) = 8
assert count_coprime(100, 6) == sum(1 for x in range(1, 101) if gcd(x, 6) == 1)
assert all(count_coprime(n, m) == sum(gcd(x, m) == 1 for x in range(1, n + 1))
for n in range(0, 60, 7) for m in range(1, 40))For a range compute .
Example 3: multiples of at least one number
How many integers in are divisible by at least one of ? Now the union formula applies, and the intersection of multiples of a subset is the multiples of their lcm:
from math import lcm
def multiples_of_any(n, numbers):
k = len(numbers)
total = 0
for mask in range(1, 1 << k):
l, bits = 1, 0
for i in range(k):
if mask >> i & 1:
l = lcm(l, numbers[i])
bits += 1
if l > n:
break
else:
total += (-1) ** (bits - 1) * (n // l)
return total
assert multiples_of_any(100, [3, 5]) == 33 + 20 - 6
assert multiples_of_any(1000, [2, 3, 5, 7]) == sum(any(x % a == 0 for a in (2, 3, 5, 7)) for x in range(1, 1001))
assert multiples_of_any(50, [4, 6, 10]) == sum(any(x % a == 0 for a in (4, 6, 10)) for x in range(1, 51))(math.lcm needs Python 3.9+.) Stopping as soon as the lcm exceeds prunes many subsets.
Example 4: sums with upper bounds
The number of solutions of in non-negative integers with : start from all solutions without upper bounds (stars and bars), and use inclusion-exclusion over the set of variables that violate their bound by setting . See Stars and Bars.
Example 5: surjections
The number of functions from an -element set onto an -element set is the number with no unused value:
from itertools import product
def surjections(n, m):
return sum((-1) ** k * comb(m, k) * (m - k) ** n for k in range(m + 1))
def surjections_brute(n, m):
return sum(len(set(f)) == m for f in product(range(m), repeat=n))
assert surjections(4, 2) == 14 and surjections(3, 3) == 6
assert all(surjections(n, m) == surjections_brute(n, m) for n in range(1, 6) for m in range(1, 5))Counting elements in exactly sets
The same idea with weights counts the elements that have exactly of the properties: replace the sign by that coefficient.
Practical notes
- Complexity is exponential in the number of sets; keep , or look for structure that lets you group terms (for example, over divisors, use the Möbius function: the sum over squarefree divisors).
- The alternating signs make intermediate sums large, but Python integers are exact. Modulo a prime, keep the signs separately and reduce carefully.
Practice problems
- UVA #10325 "The Lottery" [difficulty: low]
- UVA #11806 "Cheerleaders" [difficulty: low]
- TopCoder SRM 477 "CarelessSecretary" [difficulty: low]
- TopCoder TCHS 16 "Divisibility" [difficulty: low]
- SPOJ #6285 NGM2 , "Another Game With Numbers" [difficulty: low]
- TopCoder SRM 382 "CharmingTicketsEasy" [difficulty: medium]
- TopCoder SRM 390 "SetOfPatterns" [difficulty: medium]
- TopCoder SRM 176 "Deranged" [difficulty: medium]
- TopCoder SRM 457 "TheHexagonsDivOne" [difficulty: medium]
- SPOJ #4191 MSKYCODE "Sky Code" [difficulty: medium]
- SPOJ #4168 SQFREE "Square-free integers" [difficulty: medium]
- CodeChef "Count Relations" [difficulty: medium]
- SPOJ - Almost Prime Numbers Again
- SPOJ - Find number of Pair of Friends
- SPOJ - Balanced Cow Subsets
- SPOJ - EASY MATH [difficulty: medium]
- SPOJ - MOMOS - FEASTOFPIGS [difficulty: easy]
- Atcoder - Grid 2 [difficulty: easy]
- Codeforces - Count GCD