Math
Count elements in a union of sets by adding individual counts, subtracting pairwise intersections, adding triple intersections, and so on. Often combined with binary search on the answer.
Consider LeetCode 878. Nth Magical Number: given two positive integers a and b, find the n-th positive integer that is divisible by a or b. For small n you could iterate through every integer and check, but n can be up to , so brute force is hopeless. The key insight is a counting formula from combinatorics called the inclusion-exclusion principle, combined with binary search on the answer.
If A is the set of multiples of a and B is the set of multiples of b, then the set of numbers divisible by a or b is . The inclusion-exclusion principle says:
$$
Why subtract ? Because numbers divisible by both a and b get counted once in and once in , so we have double-counted them. Subtracting the intersection corrects for that.
What are the numbers divisible by both a and b? They are exactly the multiples of , the least common multiple. And .
So the count of positive integers up to x that are divisible by a or b is:
$$
Let a = 2, b = 3, and n = 4. We want the 4th number divisible by 2 or 3.
The sequence is: 2, 3, 4, 6, 8, 9, 10, 12, ... so the answer is 6.
First, . Now let us evaluate the counting function at a few values:
Notice the subtraction in action: 6 is divisible by both 2 and 3, so without the correction, we would have counted it twice and gotten 5 instead of 4.
The counting function is non-decreasing: as x grows, the count can only stay the same or increase. This monotonicity means we can binary search for the smallest x where .
The search bounds are straightforward. The answer is at least (the first magical number) and at most (if every magical number were a multiple of the smaller value, the n-th one would be at most this). In practice, using lo = 1 and hi = n * min(a, b) works fine.
from math import gcd
def nthMagicalNumber(n, a, b):
MOD = 10**9 + 7
lcm = a * b // gcd(a, b)
lo, hi = 1, n * min(a, b)
while lo < hi:
mid = (lo + hi) // 2
count = mid // a + mid // b - mid // lcm
if count >= n:
hi = mid
else:
lo = mid + 1
return lo % MODThe binary search finds the smallest x with at least n multiples of a or b up to x. Each iteration computes the count in using our inclusion-exclusion formula.
LeetCode 1201. Ugly Number III generalizes the idea: find the n-th positive integer divisible by a, b, or c. Now we have three sets, and the inclusion-exclusion formula extends to:
$$
The logic is the same. Adding the individual sets overcounts pairwise overlaps, so we subtract those. But then we have subtracted the triple overlap too many times (it was added 3 times and subtracted 3 times), so we add it back once.
In terms of our counting function:
$$
where .
from math import gcd
def nthUglyNumber(n, a, b, c):
def lcm(x, y):
return x * y // gcd(x, y)
ab = lcm(a, b)
ac = lcm(a, c)
bc = lcm(b, c)
abc = lcm(ab, c)
lo, hi = 1, n * min(a, b, c)
while lo < hi:
mid = (lo + hi) // 2
count = (mid // a + mid // b + mid // c
- mid // ab - mid // ac - mid // bc
+ mid // abc)
if count >= n:
hi = mid
else:
lo = mid + 1
return loThe structure is identical to the two-set version. The only difference is the counting formula, which now has seven terms instead of three.
For k sets , the inclusion-exclusion principle states:
$$
You add the sizes of individual sets, subtract the sizes of all pairwise intersections, add back all triple intersections, and so on, alternating signs. The number of terms grows exponentially: there are terms total, one for each non-empty subset of the k sets.
This means inclusion-exclusion is practical only when k is small. For you have 3 terms. For you have 7 terms. For you already have 1023 terms, which is still feasible if computing each term is cheap. But for large k, you need different techniques.
Use inclusion-exclusion when:
Use brute force or other approaches when:
A common pattern is combining inclusion-exclusion with binary search on the answer, as we saw in both problems above. The binary search handles the "find the n-th element" part, and inclusion-exclusion handles the "count elements up to x" part. This combination turns problems with enormous search spaces into binary search iterations, each costing for the inclusion-exclusion computation.
For the two-set case (LeetCode 878): binary search runs iterations. Each iteration does arithmetic plus a GCD computation (which is but typically precomputed once). Total: time, space.
For the three-set case (LeetCode 1201): same binary search cost, and each iteration evaluates 7 floor divisions in . The LCM values are precomputed. Total: time, space.
For the general k-set case: each binary search iteration requires work to enumerate all subsets. Total: time.
Compared to brute-force iteration through all integers up to the answer (which could be , easily ), this is astronomically faster. The combination of inclusion-exclusion for counting and binary search for finding the threshold is a powerful technique whenever you need the n-th element of a set defined by divisibility or union conditions.
You're probably looking at inclusion-exclusion when:
Common templates: