Facebook Pixel

1201. Ugly Number III

Problem Description

An ugly number in this problem is defined as a positive integer that is divisible by at least one of the given values a, b, or c.

Given four integers:

  • n: the position of the ugly number we want to find
  • a, b, c: the divisors that define ugly numbers

The task is to find the n-th ugly number in the sequence when ugly numbers are arranged in ascending order.

For example, if a = 2, b = 3, and c = 5, then the ugly numbers would be those divisible by 2, 3, or 5. The sequence would start as: 2, 3, 4, 5, 6, 8, 9, 10, 12, 14, 15, 16, 18, 20...

The challenge is to efficiently find the n-th number in this sequence without generating all ugly numbers up to that position, which would be inefficient for large values of n.

The solution uses binary search combined with the inclusion-exclusion principle to count how many ugly numbers exist up to a given value x. By finding the smallest x such that exactly n ugly numbers are less than or equal to x, we can determine the n-th ugly number.

The counting formula uses:

  • Add numbers divisible by a, b, or c individually
  • Subtract numbers divisible by pairs (to avoid double counting)
  • Add back numbers divisible by all three (to correct for triple subtraction)

This approach allows finding the answer in O(log(2×10^9)) time complexity.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is to reframe the problem: instead of directly finding the n-th ugly number, we ask "what is the smallest number x such that there are exactly n ugly numbers less than or equal to x?"

This reframing is powerful because:

  1. If we can count how many ugly numbers exist up to any value x, we can use binary search to find our answer
  2. Counting ugly numbers up to x is much easier than generating them in sequence

To count ugly numbers up to x, we need to count how many numbers from 1 to x are divisible by a, b, or c. Initially, we might think to just add x//a + x//b + x//c, but this has a problem - we're counting some numbers multiple times!

For instance, if a=2 and b=3, the number 6 is divisible by both, so it gets counted twice. This leads us to the inclusion-exclusion principle:

  • First, include all numbers divisible by a, b, or c
  • Then exclude (subtract) numbers we counted twice - those divisible by both a and b, both a and c, or both b and c
  • But wait! Numbers divisible by all three (like 30 when a=2, b=3, c=5) were added 3 times initially, then subtracted 3 times, so they're completely missing. We need to add them back once.

This gives us the formula:

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

Now we can use binary search on the range [1, 2×10^9]. For each midpoint, we check if it contains at least n ugly numbers. If yes, the answer might be this value or smaller; if no, we need a larger value. The binary search converges to the exact n-th ugly number.

Learn more about Math, Binary Search and Combinatorics patterns.

Solution Approach

The implementation uses the binary search template combined with the inclusion-exclusion principle to efficiently find the n-th ugly number.

Step 1: Pre-calculate LCMs First, we calculate the least common multiples (LCMs) that we'll need for the inclusion-exclusion formula:

  • ab = lcm(a, b) - for numbers divisible by both a and b
  • bc = lcm(b, c) - for numbers divisible by both b and c
  • ac = lcm(a, c) - for numbers divisible by both a and c
  • abc = lcm(a, b, c) - for numbers divisible by all three

Step 2: Define the Feasible Function The feasible function checks if there are at least n ugly numbers less than or equal to x:

def feasible(x):
    count = x//a + x//b + x//c - x//lcm_ab - x//lcm_bc - x//lcm_ac + x//lcm_abc
    return count >= n

This creates a boolean array pattern like: [false, false, ..., true, true, true] where we want to find the first true (the smallest x where count >= n).

Step 3: Binary Search Using the Template We use the standard binary search template:

left, right = 1, 2 * 10**9
first_true_index = -1

while left <= right:
    mid = (left + right) // 2
    if feasible(mid):
        first_true_index = mid
        right = mid - 1
    else:
        left = mid + 1

return first_true_index

Why this works:

  • The search space is [1, 2×10^9] where we're looking for the smallest x such that there are at least n ugly numbers ≤ x
  • first_true_index tracks the smallest value found so far where feasible(mid) is true
  • When feasible(mid) is true, we save mid as a candidate and search left for a potentially smaller answer
  • When feasible(mid) is false, we search right for a larger value
  • The loop uses while left <= right to ensure we check all candidates

Time Complexity: O(log(2×10^9))O(31) = O(1) for the binary search iterations, with O(1) work per iteration.

Space Complexity: O(1) as we only use a constant amount of extra space.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's find the 4th ugly number where a = 2, b = 3, and c = 5.

Step 1: Pre-calculate LCMs

  • lcm(2, 3) = 6
  • lcm(2, 5) = 10
  • lcm(3, 5) = 15
  • lcm(2, 3, 5) = 30

Step 2: Binary Search Setup

  • Initial range: [l=1, r=2000000000]

Step 3: Binary Search Iterations

Iteration 1:

  • mid = 1000000000
  • Count ugly numbers ≤ 1000000000:
    • Divisible by 2: 1000000000/2 = 500000000
    • Divisible by 3: 1000000000/3 = 333333333
    • Divisible by 5: 1000000000/5 = 200000000
    • Divisible by both 2 and 3 (lcm=6): 1000000000/6 = 166666666
    • Divisible by both 2 and 5 (lcm=10): 1000000000/10 = 100000000
    • Divisible by both 3 and 5 (lcm=15): 1000000000/15 = 66666666
    • Divisible by all three (lcm=30): 1000000000/30 = 33333333
    • Total: 500000000 + 333333333 + 200000000 - 166666666 - 100000000 - 66666666 + 33333333 = 733333334
  • Since 733333334 >= 4, search left half: r = 1000000000

Iteration 2:

  • mid = 500000000
  • Count = 366666667 (using same formula)
  • Since 366666667 >= 4, search left half: r = 500000000

[Many iterations later, as we narrow down...]

Near-final iterations:

  • Range narrows to [l=4, r=8]
  • mid = 6
  • Count ugly numbers ≤ 6:
    • Divisible by 2: 6/2 = 3 → {2, 4, 6}
    • Divisible by 3: 6/3 = 2 → {3, 6}
    • Divisible by 5: 6/5 = 1 → {5}
    • Divisible by 6: 6/6 = 1 → {6} (already counted in both 2 and 3)
    • Divisible by 10: 6/10 = 0 → none
    • Divisible by 15: 6/15 = 0 → none
    • Divisible by 30: 6/30 = 0 → none
    • Total: 3 + 2 + 1 - 1 - 0 - 0 + 0 = 5
  • Since 5 >= 4, search left half: r = 6

Next iteration:

  • Range is [l=4, r=6]
  • mid = 5
  • Count ugly numbers ≤ 5:
    • Divisible by 2: 5/2 = 2 → {2, 4}
    • Divisible by 3: 5/3 = 1 → {3}
    • Divisible by 5: 5/5 = 1 → {5}
    • No overlaps (6 > 5, 10 > 5, 15 > 5, 30 > 5)
    • Total: 2 + 1 + 1 - 0 - 0 - 0 + 0 = 4
  • Since 4 >= 4, search left half: r = 5

Final iteration:

  • Range is [l=4, r=5]
  • mid = 4
  • Count ugly numbers ≤ 4:
    • Divisible by 2: 4/2 = 2 → {2, 4}
    • Divisible by 3: 4/3 = 1 → {3}
    • Divisible by 5: 4/5 = 0 → none
    • Total: 2 + 1 + 0 - 0 - 0 - 0 + 0 = 3
  • Since 3 < 4, search right half: l = 5

Result: l = r = 5

The 4th ugly number is 5. We can verify: the sequence is {2, 3, 4, 5, ...}, and indeed 5 is the 4th number.

Solution Implementation

1from math import gcd
2
3class Solution:
4    def nthUglyNumber(self, n: int, a: int, b: int, c: int) -> int:
5        # Helper function to calculate Least Common Multiple
6        def lcm(x: int, y: int) -> int:
7            return x * y // gcd(x, y)
8
9        # Calculate LCMs for pairs and triplet
10        lcm_ab = lcm(a, b)
11        lcm_bc = lcm(b, c)
12        lcm_ac = lcm(a, c)
13        lcm_abc = lcm(lcm_ab, c)
14
15        def feasible(x: int) -> bool:
16            """Check if there are at least n ugly numbers <= x."""
17            count = (x // a + x // b + x // c
18                    - x // lcm_ab - x // lcm_bc - x // lcm_ac
19                    + x // lcm_abc)
20            return count >= n
21
22        # Binary search range: minimum is 1, maximum is 2 * 10^9
23        left, right = 1, 2 * 10**9
24        first_true_index = -1
25
26        # Binary search to find the smallest x where count >= n
27        while left <= right:
28            mid = (left + right) // 2
29
30            if feasible(mid):
31                first_true_index = mid
32                right = mid - 1
33            else:
34                left = mid + 1
35
36        return first_true_index
37
1class Solution {
2    private long lcmAB, lcmBC, lcmAC, lcmABC;
3    private int a, b, c, n;
4
5    public int nthUglyNumber(int n, int a, int b, int c) {
6        this.n = n;
7        this.a = a;
8        this.b = b;
9        this.c = c;
10
11        // Calculate LCM for all pairs and triplet
12        lcmAB = lcm(a, b);
13        lcmBC = lcm(b, c);
14        lcmAC = lcm(a, c);
15        lcmABC = lcm(lcmAB, c);
16
17        // Binary search range: minimum is 1, maximum is 2 * 10^9
18        long left = 1;
19        long right = 2000000000L;
20        long firstTrueIndex = -1;
21
22        // Binary search to find the smallest x where count >= n
23        while (left <= right) {
24            long mid = left + (right - left) / 2;
25
26            if (feasible(mid)) {
27                firstTrueIndex = mid;
28                right = mid - 1;
29            } else {
30                left = mid + 1;
31            }
32        }
33
34        return (int) firstTrueIndex;
35    }
36
37    private boolean feasible(long x) {
38        // Count ugly numbers <= x using inclusion-exclusion principle
39        long count = x / a + x / b + x / c
40                    - x / lcmAB - x / lcmBC - x / lcmAC
41                    + x / lcmABC;
42        return count >= n;
43    }
44
45    private long gcd(long a, long b) {
46        return b == 0 ? a : gcd(b, a % b);
47    }
48
49    private long lcm(long a, long b) {
50        return a * b / gcd(a, b);
51    }
52}
53
1class Solution {
2public:
3    int nthUglyNumber(int n, int a, int b, int c) {
4        // Calculate LCM for all pairs and triplet
5        long long lcm_ab = calculateLCM(a, b);
6        long long lcm_bc = calculateLCM(b, c);
7        long long lcm_ac = calculateLCM(a, c);
8        long long lcm_abc = calculateLCM(lcm_ab, c);
9
10        // Feasible function: check if there are at least n ugly numbers <= x
11        auto feasible = [&](long long x) {
12            long long count = x / a + x / b + x / c
13                            - x / lcm_ab - x / lcm_bc - x / lcm_ac
14                            + x / lcm_abc;
15            return count >= n;
16        };
17
18        // Binary search range: [1, 2*10^9]
19        long long left = 1;
20        long long right = 2000000000LL;
21        long long firstTrueIndex = -1;
22
23        // Binary search to find the smallest x where count >= n
24        while (left <= right) {
25            long long mid = left + (right - left) / 2;
26
27            if (feasible(mid)) {
28                firstTrueIndex = mid;
29                right = mid - 1;
30            } else {
31                left = mid + 1;
32            }
33        }
34
35        return firstTrueIndex;
36    }
37
38private:
39    long long calculateLCM(long long a, long long b) {
40        return a * b / calculateGCD(a, b);
41    }
42
43    long long calculateGCD(long long a, long long b) {
44        return b == 0 ? a : calculateGCD(b, a % b);
45    }
46};
47
1function nthUglyNumber(n: number, a: number, b: number, c: number): number {
2    // Calculate least common multiples for pairs and triplet
3    const lcmAB = lcm(BigInt(a), BigInt(b));
4    const lcmBC = lcm(BigInt(b), BigInt(c));
5    const lcmAC = lcm(BigInt(a), BigInt(c));
6    const lcmABC = lcm(BigInt(a), lcmBC);
7
8    // Feasible function: check if there are at least n ugly numbers <= x
9    const feasible = (x: bigint): boolean => {
10        const count =
11            x / BigInt(a) +
12            x / BigInt(b) +
13            x / BigInt(c) -
14            x / lcmAB -
15            x / lcmBC -
16            x / lcmAC +
17            x / lcmABC;
18        return count >= BigInt(n);
19    };
20
21    // Binary search range
22    let left = 1n;
23    let right = BigInt(2e9);
24    let firstTrueIndex = -1n;
25
26    // Binary search to find the smallest x where count >= n
27    while (left <= right) {
28        const mid = (left + right) / 2n;
29
30        if (feasible(mid)) {
31            firstTrueIndex = mid;
32            right = mid - 1n;
33        } else {
34            left = mid + 1n;
35        }
36    }
37
38    return Number(firstTrueIndex);
39}
40
41function gcd(a: bigint, b: bigint): bigint {
42    return b === 0n ? a : gcd(b, a % b);
43}
44
45function lcm(a: bigint, b: bigint): bigint {
46    return (a * b) / gcd(a, b);
47}
48

Time and Space Complexity

The time complexity is O(log m), where m = 2 × 10^9. This is because the algorithm uses binary search on the range [1, 2 × 10^9]. In each iteration, the search space is halved, requiring at most log₂(2 × 10^9) ≈ 31 iterations. Within each iteration, the operations performed are:

  • Computing mid = (l + r) >> 1: O(1)
  • Calculating the count using inclusion-exclusion principle with divisions: mid // a + mid // b + mid // c - mid // ab - mid // bc - mid // ac + mid // abc: O(1)
  • Comparison with n: O(1)

Since all operations inside the loop are O(1) and the loop runs O(log m) times, the overall time complexity is O(log m).

The space complexity is O(1). The algorithm only uses a constant amount of extra space to store:

  • Variables ab, bc, ac, abc for the least common multiples
  • Variables l, r, mid for the binary search bounds and midpoint
  • The comparison result

All these variables require constant space regardless of the input size n, making the space complexity O(1).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Using Wrong Binary Search Template Variant

A common mistake is using while left < right with right = mid instead of the standard template with while left <= right, first_true_index, and right = mid - 1.

Problem Example:

# Non-standard variant - harder to reason about
while left < right:
    mid = (left + right) // 2
    if count >= n:
        right = mid
    else:
        left = mid + 1
return left

Solution:

# Standard template - clear and consistent
first_true_index = -1
while left <= right:
    mid = (left + right) // 2
    if feasible(mid):
        first_true_index = mid
        right = mid - 1
    else:
        left = mid + 1
return first_true_index

2. Integer Overflow in LCM Calculation

The most critical pitfall is calculating LCM using the formula lcm(x, y) = x * y / gcd(x, y). When x and y are large, their product can overflow.

Problem Example:

# Incorrect - multiplication happens first, can overflow
def lcm(x, y):
    return x * y // gcd(x, y)  # x * y might overflow before division

Solution:

# Better - divide first to reduce overflow risk
def lcm(x, y):
    return x // gcd(x, y) * y  # Divide first, then multiply

3. Incorrect Order of LCM Calculations for Three Numbers

When calculating lcm(a, b, c), a common mistake is assuming it equals a * b * c / gcd(a, b, c).

Problem Example:

# Incorrect approaches
lcm_abc = a * b * c // gcd(gcd(a, b), c)  # Wrong formula

Solution:

# Correct - LCM is associative
lcm_abc = lcm(lcm(a, b), c)

4. Forgetting Edge Cases in Inclusion-Exclusion

Not handling the case where some of the numbers (a, b, c) might be equal or one divides another, leading to incorrect LCM calculations or double counting.

Solution: The provided code handles this correctly by using proper LCM calculation with GCD, which automatically handles cases where numbers share common factors.

5. Using Wrong Binary Search Range

Setting an insufficient upper bound for the binary search can cause the answer to be outside the search range.

Solution:

# Safe upper bound per problem constraints
right = 2 * 10**9
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Is the following code DFS or BFS?

void search(Node root) {
  if (!root) return;
  visit(root);
  root.visited = true;
  for (Node node in root.adjacent) {
    if (!node.visited) {
      search(node);
    }
  }
}

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More