Skip to content
← All posts

Ugly Number III: Binary Search and Inclusion-Exclusion

7 min read
leetcodeproblemmediummathbinary-search

Ugly Number III is LeetCode 1201. Given four positive integers n, a, b, and c, find the nth positive integer that is divisible by a, b, or c. This is a medium-difficulty problem, but the technique behind it is genuinely elegant: binary search on the answer combined with the inclusion-exclusion principle.

If you have seen Nth Magical Number, you will recognize the shape of this problem. Ugly Number III extends the same idea from two divisors to three, which makes the counting formula a bit more involved but does not change the overall approach.

Numbers divisible by a=2, b=3, or c=5 (first 20)12#13#24#35#46#578#69#710#81112#91314#1015#1116#121718#131920#14Inclusion-exclusion count up to n:n/a + n/b + n/c - n/lcm(a,b) - n/lcm(a,c) - n/lcm(b,c) + n/lcm(a,b,c)count(10) = 5 + 3 + 2 - 1 - 1 - 0 + 0 = 8a=2b=3c=5overlap (counted once)two-way overlap
Of the first 20 positive integers, 13 are divisible by 2, 3, or 5. Overlaps like 6 (div by 2 and 3) and 30 (div by all three) must not be double-counted.

Why this problem matters

Ugly Number III sits at the intersection of two important algorithmic ideas. First, binary search on the answer space, where you do not search through an array but instead search through a range of candidate values. Second, a number-theoretic counting formula based on inclusion-exclusion. Problems like this train you to combine mathematical reasoning with algorithmic templates, a skill that shows up in many contest and interview settings.

The key insight

You cannot generate all ugly numbers one by one and stop at the nth one. With n up to 2 * 10^9, that would be far too slow. Instead, you flip the question: "Given a number x, how many positive integers up to x are divisible by a, b, or c?" If you can answer that question efficiently, you can binary search for the smallest x where the count equals n.

The count function uses inclusion-exclusion. The number of integers from 1 to x divisible by a is x // a. But if you just add x // a + x // b + x // c, you double-count numbers divisible by two of them and triple-count numbers divisible by all three. Inclusion-exclusion corrects for that:

count(x) = x//a + x//b + x//c - x//lcm(a,b) - x//lcm(a,c) - x//lcm(b,c) + x//lcm(a,b,c)

Each term runs in O(1), so the entire count is O(1). The binary search then runs in O(log(max_val)) iterations, each doing O(1) work.

The solution

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, 2 * 10**9

    while lo < hi:
        mid = lo + (hi - lo) // 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 lo

Let's break this down piece by piece.

LCM precomputation. Before the binary search starts, you precompute lcm(a, b), lcm(a, c), lcm(b, c), and lcm(a, b, c). The LCM of two numbers is x * y // gcd(x, y). For three numbers, lcm(a, b, c) = lcm(lcm(a, b), c). These values are computed once and reused in every iteration.

Search range. lo starts at 1 (the smallest possible answer) and hi starts at 2 * 10^9 (a safe upper bound since n is at most 2 * 10^9 and the smallest divisor is at least 1). You could tighten hi to n * min(a, b, c), but 2 * 10^9 is fine and avoids edge case mistakes.

The count formula. At each midpoint, you compute how many integers from 1 to mid are divisible by a, b, or c. This is the inclusion-exclusion formula. If the count is at least n, the answer might be mid or something smaller, so you set hi = mid. If the count is less than n, you need a larger value, so you set lo = mid + 1.

Convergence. When lo == hi, that is your answer.

The inclusion-exclusion formula for three sets follows a simple pattern: add the singles, subtract the pairs, add the triple. For two divisors you only need x//a + x//b - x//lcm(a,b). For three, you add one more layer. The pattern generalizes to any number of sets, alternating addition and subtraction.

Visual walkthrough

Let's trace the binary search for n=5, a=2, b=3, c=5. The LCMs are: lcm(2,3) = 6, lcm(2,5) = 10, lcm(3,5) = 15, lcm(2,3,5) = 30.

Step 1: Define range. lo=1, hi=10, mid=5

lo1hi10mid5count(5) = 5/2 + 5/3 + 5/5 - 5/6 - 5/10 - 5/15 + 5/30 = 2+1+1-0-0-0+0 = 4count(mid) = 4, target n = 5Too few (4 < 5), move lo up to mid + 1 = 6

Only 4 numbers up to 5 are divisible by 2, 3, or 5 (they are 2, 3, 4, 5). We need 5, so the answer is larger.

Step 2: lo=6, hi=10, mid=8

lo6hi10mid8count(8) = 8/2 + 8/3 + 8/5 - 8/6 - 8/10 - 8/15 + 8/30 = 4+2+1-1-0-0+0 = 6count(mid) = 6, target n = 5Enough (6 >= 5), move hi down to mid = 8

There are 6 qualifying numbers up to 8. The answer could be 8, but it might be smaller.

Step 3: lo=6, hi=8, mid=7

lo6hi8mid7count(7) = 7/2 + 7/3 + 7/5 - 7/6 - 7/10 - 7/15 + 7/30 = 3+2+1-1-0-0+0 = 5count(mid) = 5, target n = 5Enough (5 >= 5), move hi down to mid = 7

There are 5 qualifying numbers up to 7. Tightening hi to 7.

Step 4: lo=6, hi=7, mid=6

lo6hi7mid6count(6) = 6/2 + 6/3 + 6/5 - 6/6 - 6/10 - 6/15 + 6/30 = 3+2+1-1-0-0+0 = 5count(mid) = 5, target n = 5Enough (5 >= 5), move hi down to mid = 6

5 qualifying numbers up to 6 as well. We tighten hi to 6.

Step 5: lo=6, hi=6. Converged!

lo6hi6mid6count(6) = 6/2 + 6/3 + 6/5 - 6/6 - 6/10 - 6/15 + 6/30 = 5count(mid) = 5, target n = 5Found: the 5th ugly number is 6

lo equals hi, so we have our answer. The 5th number divisible by 2, 3, or 5 is 6.

Five steps to converge from the range [1, 10] down to the single answer 6. The 5th positive integer divisible by 2, 3, or 5 is indeed 6 (the sequence is 2, 3, 4, 5, 6). With the actual constraints where hi can be up to 2 * 10^9, the binary search still takes at most about 31 iterations, each doing constant work. That is why this approach is so fast.

Complexity analysis

ApproachTimeSpace
Binary search + inclusion-exclusionO(log(max_val))O(1)

Time: O(log(max_val)). The binary search runs over a range up to 2 * 10^9, so at most about 31 iterations. Each iteration computes the count in O(1). The GCD and LCM precomputations are also O(log(max(a, b, c))) but that is dominated by the binary search.

Space: O(1). You store a handful of variables for the LCMs and the binary search bounds. No extra data structures needed.

The building blocks

1. Binary search on the answer space

The core template is the same one you see in Koko Eating Bananas and Capacity to Ship Packages:

lo, hi = 1, 2 * 10**9

while lo < hi:
    mid = lo + (hi - lo) // 2
    if is_feasible(mid):
        hi = mid
    else:
        lo = mid + 1

return lo

You define a range of candidate answers, check a condition at the midpoint, and narrow down. The condition here is count(mid) >= n instead of a pile-eating feasibility check, but the binary search skeleton is identical.

2. Inclusion-exclusion counting

The counting function is the math that makes this problem unique:

def count(x, a, b, c, ab, ac, bc, abc):
    return x // a + x // b + x // c - x // ab - x // ac - x // bc + x // abc

This counts how many integers in [1, x] are divisible by at least one of a, b, or c. Without inclusion-exclusion, you would overcount numbers like 6 (divisible by both 2 and 3) or 30 (divisible by all three). The alternating add-subtract pattern ensures each number is counted exactly once.

Edge cases

  • Two or all three divisors are equal: if a == b, then lcm(a, b) = a, and the formula naturally reduces. No special handling needed.
  • One divisor divides another: if a divides b, then every multiple of b is also a multiple of a. The LCM is b, and inclusion-exclusion handles it correctly.
  • n = 1: the answer is min(a, b, c), the smallest of the three divisors. The binary search finds this naturally.
  • Very large n: the upper bound 2 * 10^9 is safe for the constraints. You could also use n * min(a, b, c) as the upper bound for a tighter range.
  • Integer overflow: in Python, integers have arbitrary precision, so this is not a concern. In languages like Java or C++, you need to use long for the LCM computations.

From understanding to recall

The structure of this problem is clean: binary search the answer space, count with inclusion-exclusion, narrow down. You probably follow the logic right now.

But reconstructing it under time pressure is another matter. Was the count formula + + + - - - + or + + - + - +? Do you precompute lcm(a, b, c) as lcm(lcm(a, b), c) or something else? Is the upper bound n * min(a, b, c) or 2 * 10^9? These details are easy to mix up if you have not practiced them recently.

Spaced repetition locks in the pattern. You write the solution from scratch today, again tomorrow, then in a few days. After a few rounds, the inclusion-exclusion formula and the binary search template are automatic. You do not rederive them. You just write them.

LeetCode 1201 is a great SRS candidate because the code is compact (about 15 lines), the math is reusable in related problems, and the formula has just enough terms to trip you up if you have not drilled it.

Related posts

  • Binary Search (LeetCode) - The foundational binary search implementation that underpins the search-on-answer pattern
  • Koko Eating Bananas - The classic binary search on the answer problem, same template with a different feasibility check
  • Nth Magical Number - The two-divisor version of this exact problem, using the same inclusion-exclusion and binary search approach

CodeBricks helps you internalize patterns like binary search with inclusion-exclusion through spaced repetition. Instead of re-solving problems from scratch each time, you build lasting recall of the templates and formulas that matter most in interviews.