Inclusion-Exclusion Principle (PIE) — Middle Level¶
Focus: why PIE works and when to choose it — the complement form ("none of the properties"), the bitmask loop over
nsets, and the workhorse applications: counting integers coprime to a value (linking to Euler's totient and Möbius), counting integers divisible by at least one of a set via LCM, derangements, and surjection counting. You also learn when2ⁿis acceptable and when to switch to the Möbius/divisor view.
Table of Contents¶
- Introduction
- Deeper Concepts
- Comparison with Alternatives
- Advanced Patterns
- Counting Coprime and Divisible-By
- Derangements and Surjections
- Code Examples
- Error Handling
- Performance Analysis
- Best Practices
- Visual Animation
- Summary
Introduction¶
At junior level the message was the alternating formula: + singles, − pairs, + triples. At middle level you turn that formula into reliable engineering decisions:
- Why does the alternation produce exactly-once counting? (Every element is counted once in the union; the proof is a tiny binomial identity.)
- When is the complement form ("none of the properties") cleaner than the direct union?
- How do you iterate the
2ⁿsubsets efficiently with a bitmask, and how do you know2ⁿis affordable? - How does PIE compute the count of integers coprime to
m— and how is that the same object as Euler's totientφ(m)and a Möbius sum? - How do derangements (permutations with no fixed point) and surjection counts fall out of PIE in one line each?
These questions separate "I can plug into the formula" from "I can recognize a PIE problem, pick the direct-or-complement form, and bound its cost."
Deeper Concepts¶
Why every element is counted exactly once¶
Take an element x that lies in exactly t of the sets (t ≥ 1, so it is in the union and should contribute 1). How many PIE terms contain x? A term is an intersection of some subset S of the sets; x is in that intersection iff S is a subset of the t sets containing x. The number of size-k such subsets is C(t, k), and each contributes with sign (−1)^(k+1). So x's total contribution is:
Σ_{k=1}^{t} (−1)^(k+1) C(t, k)
= −Σ_{k=1}^{t} (−1)^k C(t, k)
= −[ (Σ_{k=0}^{t} (−1)^k C(t, k)) − 1 ]
= −[ (1 − 1)^t − 1 ]
= −[ 0 − 1 ] = 1.
The binomial theorem (1 − 1)^t = 0 makes all the over-counts cancel, leaving exactly 1. An element in zero sets appears in no term and contributes 0. So the signed sum counts each union member exactly once — that is the entire correctness argument, and the full proof lives in professional.md.
The complement form as the natural starting point¶
The "none of the properties" count is:
where the empty subset S = ∅ contributes (−1)^0 |whole universe| = +N. This is the same alternating sum as the union, just folded so it begins at the universe. Many problems are stated as "none" — count numbers coprime to m (divisible by none of its primes), count permutations with no fixed point (derangements), count colorings avoiding all forbidden patterns. Reaching for the complement form first often makes the sets Aᵢ (the "bad" events) easier to describe than the union.
The bitmask correspondence¶
Each subset S ⊆ {0, …, n−1} is encoded by an integer mask ∈ [0, 2ⁿ) where bit i is set iff i ∈ S. Then:
mask = 0↔ empty subset (the universe term in the complement form).popcount(mask)=|S|= the number of sets in this intersection → drives the sign.- Looping
maskfrom1to2ⁿ − 1enumerates every non-empty subset (the union form); from0includes the universe (the complement form).
This is the single most important implementation idiom for PIE: the whole principle is one for mask loop.
When the term is zero — pruning¶
If an intersection is empty (or its count is 0, e.g. ⌊N/lcm⌋ = 0 once lcm > N), the term contributes nothing. Recognizing and skipping such subtrees is the basis of pruning the 2ⁿ blowup (covered in senior.md): once a partial LCM exceeds N, every superset of that subset also has a zero term, so you can prune the entire branch.
Comparison with Alternatives¶
Choosing how to count "at least one / none"¶
| Method | Best when | Cost | Notes |
|---|---|---|---|
| PIE (bitmask) | few properties (n ≤ ~20), huge universe N | O(2ⁿ · n) | exact, independent of N |
| Brute force | small universe N, any n | O(N · n) | the correctness oracle |
| Möbius over prime factors | "coprime / divisible by primes of m" | O(2^ω(m)) or O(√m) to factor | PIE specialized to divisors (sibling 32) |
Sieve (e.g. linear sieve of φ) | totients of a whole range | O(n log log n) | when you need many answers at once (sibling 04) |
| Direct DP / recurrence | structured constraints (derangements) | O(n) | once PIE derives the recurrence |
The crossover is clear: PIE wins when n (number of properties) is small and N is large; brute force wins when N is small; sieve/Möbius win when you need answers for an entire range or when the "sets" are the prime factors of a single number.
Direct union vs complement¶
Direct union (|A₁ ∪ … ∪ Aₙ|) | Complement (#none) | |
|---|---|---|
| Loop range | mask = 1 .. 2ⁿ−1 | mask = 0 .. 2ⁿ−1 (or N − union) |
| First term | +Σ|Aᵢ| | +N (universe) |
| Natural for | "how many satisfy at least one?" | "how many satisfy none?" (coprime, derangement) |
Sign of k-set term | (−1)^(k+1) | (−1)^k |
They are algebraically identical (#none = N − union); pick whichever matches the problem statement to minimize setup mistakes.
Same-size collapse vs full bitmask¶
Full bitmask (2ⁿ) | Same-size collapse (n+1) | |
|---|---|---|
| When valid | always | only when \|A_S\| depends only on \|S\|=k |
| Term count | 2ⁿ − 1 (or 2ⁿ) | n + 1 (one per k) |
| Sign source | popcount(mask) | (−1)^k per group |
| Examples | divisible-by, dedup over distinct sets | derangements (n−k)!, surjections (m−k)^n |
| Cost | O(2ⁿ · n) | O(n) |
The collapse is the single most important 2ⁿ → n optimization at this level: when every size-k subset has the same intersection size, the C(n,k) identical terms merge into one C(n,k)·(−1)^k·|term| summand. Derangements and surjections are exactly this case; divisible-by-at-least-one is not (each subset has a different LCM), so it stays 2ⁿ.
Advanced Patterns¶
Pattern: Generic PIE over n sets via a callback¶
Go¶
package main
import "fmt"
// pie computes the inclusion-exclusion sum where countIntersection(mask)
// returns |⋂ of the sets selected by mask|. Pass includeEmpty=true for the
// complement ("none") form, false for the union ("at least one") form.
func pie(n int, includeEmpty bool, countIntersection func(mask int) int64) int64 {
start := 1
if includeEmpty {
start = 0
}
var total int64
for mask := start; mask < (1 << n); mask++ {
bits := popcount(mask)
term := countIntersection(mask)
if bits%2 == 0 {
total += term // even (incl. empty): + in complement form
} else {
total -= term
}
}
return total
}
func popcount(x int) int {
c := 0
for x != 0 {
x &= x - 1
c++
}
return c
}
func main() {
// Count integers in [1,30] coprime to 30 = none divisible by 2,3,5.
primes := []int64{2, 3, 5}
N := int64(30)
gcd := func(a, b int64) int64 {
for b != 0 {
a, b = b, a%b
}
return a
}
res := pie(len(primes), true, func(mask int) int64 {
l := int64(1)
for i := 0; i < len(primes); i++ {
if mask&(1<<i) != 0 {
l = l / gcd(l, primes[i]) * primes[i]
}
}
return N / l
})
fmt.Println(res) // 8 = φ(30)
}
Java¶
public class GenericPIE {
interface Counter { long count(int mask); }
static long pie(int n, boolean includeEmpty, Counter c) {
long total = 0;
int start = includeEmpty ? 0 : 1;
for (int mask = start; mask < (1 << n); mask++) {
int bits = Integer.bitCount(mask);
long term = c.count(mask);
if (bits % 2 == 0) total += term; // even (incl. empty)
else total -= term;
}
return total;
}
static long gcd(long a, long b) { while (b != 0) { long t = a % b; a = b; b = t; } return a; }
public static void main(String[] args) {
long[] primes = {2, 3, 5};
long N = 30;
long res = pie(primes.length, true, mask -> {
long l = 1;
for (int i = 0; i < primes.length; i++)
if ((mask & (1 << i)) != 0) l = l / gcd(l, primes[i]) * primes[i];
return N / l;
});
System.out.println(res); // 8 = φ(30)
}
}
Python¶
from math import gcd
def pie(n, include_empty, count_intersection):
"""Generic inclusion-exclusion driver. count_intersection(mask) -> |⋂ chosen|."""
total = 0
start = 0 if include_empty else 1
for mask in range(start, 1 << n):
bits = bin(mask).count("1")
term = count_intersection(mask)
if bits % 2 == 0: # even subset (incl. empty) -> +
total += term
else:
total -= term
return total
if __name__ == "__main__":
primes, N = [2, 3, 5], 30
def count(mask):
l = 1
for i, p in enumerate(primes):
if mask & (1 << i):
l = l // gcd(l, p) * p
return N // l
print(pie(len(primes), include_empty=True, count_intersection=count)) # 8 = φ(30)
Pattern: Möbius-flavored sign from squarefree divisors¶
When the sets are the distinct prime factors of m, each subset corresponds to a squarefree divisor d (the product of the chosen primes), and the sign (−1)^(number of primes) is exactly the Möbius function μ(d). So the complement (coprime) count becomes:
where rad(m) is the product of distinct primes of m. This is PIE rewritten as a Möbius sum — the bridge to sibling 32-mobius-inversion.
Counting Coprime and Divisible-By¶
Coprime to m in [1, N]¶
"Coprime to m" means "divisible by none of m's prime factors". Let the distinct primes of m be p₁, …, p_r. By the complement form:
When N = m, this is precisely Euler's totient φ(m) — the inclusion-exclusion derivation of the totient formula φ(m) = m ∏(1 − 1/p):
So the totient is a PIE computation over the prime factors. (See sibling 05-fermat-euler for the totient in depth.)
Divisible by at least one of k₁, …, kₙ¶
The direct union form, with each pairwise (and higher) intersection counted via the LCM of the chosen kᵢ:
The only subtlety versus the coprime case: the kᵢ need not be primes, so intersections use lcm, not the product. If kᵢ are pairwise coprime, lcm = product and the two formulas coincide.
Worked numeric: coprime to 12 in [1, 12]¶
12 = 2² · 3, distinct primes {2, 3}:
The coprime residues are {1, 5, 7, 11} — exactly 4, confirming PIE = totient.
Derangements and Surjections¶
Derangements: permutations with no fixed point¶
A derangement of {1, …, n} is a permutation where no element stays in its own position. Let Aᵢ = permutations fixing position i. We want permutations in none of the Aᵢ. There are n! permutations total; |⋂_{i∈S} Aᵢ| = (n − |S|)! (fix the |S| chosen positions, permute the rest freely). By the complement form:
That tidy alternating sum gives D(0)=1, D(1)=0, D(2)=1, D(3)=2, D(4)=9, D(5)=44, and D(n)/n! → 1/e ≈ 0.3679. PIE turns a messy "avoid all fixed points" question into a one-line sum.
Surjections (onto functions) and Stirling numbers¶
How many functions from an n-element set onto an m-element set are surjective (hit every output)? Let Aᵢ = functions that miss output i. We want functions in none of the Aᵢ. There are m^n functions total; |⋂_{i∈S} Aᵢ| = (m − |S|)^n (functions avoiding the |S| chosen outputs). By PIE:
Dividing by m! gives the Stirling number of the second kind S(n, m) = Surj(n,m)/m!, the count of ways to partition n items into m non-empty unlabeled groups. So PIE is the standard derivation of the surjection count and Stirling numbers.
Comparison: which PIE setup for which problem¶
| Problem | Universe | "Bad" set Aᵢ | Intersection size |
|---|---|---|---|
Coprime to m | [1, N] | divisible by prime pᵢ | ⌊N / ∏ pᵢ⌋ |
Divisible by ≥1 of kᵢ | [1, N] | divisible by kᵢ | ⌊N / lcm(kᵢ)⌋ |
| Derangements | all n! perms | fixes position i | (n − |S|)! |
Surjections onto m | all m^n funcs | misses output i | (m − |S|)^n |
| Forbidden positions | placements | uses forbidden cell i | problem-specific (rook polynomial) |
Code Examples¶
Derangements via PIE¶
Python¶
from math import comb, factorial
def derangements(n):
"""D(n) = sum_{k=0}^{n} (-1)^k C(n,k) (n-k)! via inclusion-exclusion."""
return sum((-1) ** k * comb(n, k) * factorial(n - k) for k in range(n + 1))
if __name__ == "__main__":
print([derangements(n) for n in range(7)]) # [1, 0, 1, 2, 9, 44, 265]
Go¶
package main
import "fmt"
func factorial(n int) int64 {
r := int64(1)
for i := int64(2); i <= int64(n); i++ {
r *= i
}
return r
}
func comb(n, k int) int64 {
if k < 0 || k > n {
return 0
}
r := int64(1)
for i := 0; i < k; i++ {
r = r * int64(n-i) / int64(i+1)
}
return r
}
func derangements(n int) int64 {
var total int64
for k := 0; k <= n; k++ {
term := comb(n, k) * factorial(n-k)
if k%2 == 0 {
total += term
} else {
total -= term
}
}
return total
}
func main() {
for n := 0; n <= 6; n++ {
fmt.Print(derangements(n), " ") // 1 0 1 2 9 44 265
}
fmt.Println()
}
Java¶
public class Derangements {
static long factorial(int n) {
long r = 1;
for (int i = 2; i <= n; i++) r *= i;
return r;
}
static long comb(int n, int k) {
if (k < 0 || k > n) return 0;
long r = 1;
for (int i = 0; i < k; i++) r = r * (n - i) / (i + 1);
return r;
}
static long derangements(int n) {
long total = 0;
for (int k = 0; k <= n; k++) {
long term = comb(n, k) * factorial(n - k);
if (k % 2 == 0) total += term; else total -= term;
}
return total;
}
public static void main(String[] args) {
for (int n = 0; n <= 6; n++) System.out.print(derangements(n) + " ");
// 1 0 1 2 9 44 265
System.out.println();
}
}
Surjection count via PIE (Python)¶
from math import comb
def surjections(n, m):
"""Onto functions from n-set to m-set: sum_{k} (-1)^k C(m,k) (m-k)^n."""
return sum((-1) ** k * comb(m, k) * (m - k) ** n for k in range(m + 1))
def stirling2(n, m):
return surjections(n, m) // (1 if m == 0 else __import__("math").factorial(m))
if __name__ == "__main__":
print(surjections(4, 2)) # 14 (2^4 = 16 functions, minus 2 constant)
print(surjections(3, 3)) # 6 (= 3! permutations)
print(stirling2(4, 2)) # 7
Error Handling¶
| Scenario | What goes wrong | Correct approach |
|---|---|---|
Coprime count uses all factors of m | Repeated primes (e.g. 2²) add duplicate terms. | Use only the distinct primes (the radical). |
| Divisible-by uses product not LCM | "both 4 and 6" miscounted. | Intersection size is ⌊N/lcm⌋. |
| Sign wrong in complement form | Used (−1)^(k+1) instead of (−1)^k. | Complement: empty/even subsets +, odd −. |
Surjection with m > n | Should be 0 (can't be onto). | PIE naturally yields 0; verify it does. |
Overflow in (m−k)^n or lcm | Large exponent/LCM overflows 64-bit. | Use bignum (Python), 128-bit, or mod arithmetic. |
2ⁿ loop too large | n (number of properties) too big. | Switch to Möbius/divisor structure or DP. |
Performance Analysis¶
| Task | Scale | Cost |
|---|---|---|
PIE over n properties (bitmask) | n ≤ 20 | O(2ⁿ · n) ≈ 2·10^7 ops — fast |
Divisible-by-at-least-one, N huge | N ≤ 10^18, n ≤ 20 | O(2ⁿ · n) — independent of N |
Coprime count over ω(m) distinct primes | ω(m) ≤ 15 | O(2^ω(m)) — ω(m) is tiny in practice |
| Derangements / surjections | n, m ≤ 10^6 | O(n) (linear sum, not 2ⁿ!) |
| Brute-force oracle | N ≤ 10^6 | O(N · n) |
Note the contrast: set-based PIE (divisible-by, coprime) costs 2ⁿ in the number of sets, but the derangement/surjection sums are only O(n) because their intersection sizes depend only on |S| = k, so the C(n,k) identical-size subsets collapse into a single k-indexed term. Recognizing when subsets of the same size are interchangeable is the key optimization — it turns 2ⁿ into n+1.
import time
def benchmark(n):
start = time.perf_counter()
total = 0
for mask in range(1 << n):
total += bin(mask).count("1")
return time.perf_counter() - start
# n = 22 (~4M masks) runs in well under a second in CPython.
Best Practices¶
- Pick direct vs complement to match the problem statement — "at least one" → direct union; "none / coprime / no fixed point" → complement. It minimizes setup errors.
- Collapse same-size subsets when the intersection size depends only on
|S|(derangements, surjections) — turnsO(2ⁿ)intoO(n). - Use distinct primes (the radical) for coprime counts; repeated prime powers add nothing.
- Use
lcm, not the product, for divisible-by intersections unless the values are pairwise coprime. - Always validate against the brute-force oracle on small inputs; PIE sign bugs are silent.
- Recognize the Möbius restatement for prime-factor sets — it is the same sum and links to sibling
32-mobius-inversion.
Visual Animation¶
See
animation.htmlfor interactive visualization.The middle-level animation highlights: - The Venn regions of two/three sets, each lit as its
+/−term is applied, showing how over-counts cancel to count each region once. - A bitmask subset enumeration for "divisible by at least one", displaying each mask, its chosen numbers, the LCM,⌊N/lcm⌋, the sign, and the running total. - Toggle between the direct union and complement views to see the same answer reached from both ends. - EditableNand number list so you can watch the totient emerge when the inputs are the prime factors ofN.
Summary¶
PIE is correct because every element in exactly t sets contributes Σ_{k=1}^{t} (−1)^(k+1) C(t,k) = 1 (a binomial cancellation), and 0 if in no set. The complement form #none = Σ (−1)^|S| |⋂ Aᵢ| starts from the universe and is the natural shape for "coprime / no fixed point" problems. Computationally the whole principle is one bitmask loop over 2ⁿ subsets, with the sign driven by popcount. The marquee applications: counting integers coprime to m (which is Euler's totient φ(m) = m∏(1−1/p), and rewrites as a Möbius sum over squarefree divisors — sibling 32), counting integers divisible by at least one of several values via ⌊N/lcm⌋, derangements D(n) = n!Σ(−1)^k/k!, and surjections / Stirling numbers Σ(−1)^k C(m,k)(m−k)^n. Set-based PIE is O(2ⁿ) in the number of properties (fine for n ≤ 20, independent of N), but collapses to O(n) when same-size subsets are interchangeable. Choose direct-or-complement to fit the question, use LCM for intersections, and switch to Möbius/structure when n grows.
Next step: continue to senior.md for large-scale counting (analytics, dedup), pruning the 2ⁿ blowup, and combining PIE with sieves and Möbius inversion.