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 finda
,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
, orc
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.
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:
- If we can count how many ugly numbers exist up to any value
x
, we can use binary search to find our answer - 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
, orc
- Then exclude (subtract) numbers we counted twice - those divisible by both
a
andb
, botha
andc
, or bothb
andc
- 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 binary search 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 botha
andb
bc = lcm(b, c)
- for numbers divisible by bothb
andc
ac = lcm(a, c)
- for numbers divisible by botha
andc
abc = lcm(a, b, c)
- for numbers divisible by all three
Step 2: Set up Binary Search Boundaries
- Left boundary
l = 1
(smallest possible ugly number) - Right boundary
r = 2 × 10^9
(maximum value per problem constraints)
Step 3: Binary Search Loop
While l < r
:
- Calculate midpoint:
mid = (l + r) >> 1
(using bit shift for division by 2) - Count ugly numbers up to
mid
using inclusion-exclusion:count = mid//a + mid//b + mid//c - mid//ab - mid//bc - mid//ac + mid//abc
- Make decision based on count:
- If
count >= n
: Then
-th ugly number is at mostmid
, so search in left half:r = mid
- If
count < n
: We need more ugly numbers, so search in right half:l = mid + 1
- If
Step 4: Return Result
When the loop ends, l
equals r
, which is the smallest number x
such that there are exactly n
ugly numbers less than or equal to x
. This is our answer.
Why this works:
- The binary search maintains an invariant: the answer is always in the range
[l, r]
- We're looking for the leftmost position where the count is at least
n
- When
count >= n
atmid
, we includemid
in our search range (r = mid
) becausemid
itself might be then
-th ugly number - When
count < n
, we excludemid
(l = mid + 1
) because we need a larger number
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 EvaluatorExample 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
- Divisible by 2:
- 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
- Divisible by 2:
- 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
- Divisible by 2:
- 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
- Divisible by 2:
- 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 # Binary search range: minimum is 1, maximum is 2 * 10^9
16 left, right = 1, 2 * 10**9
17
18 # Binary search to find the nth ugly number
19 while left < right:
20 mid = (left + right) // 2
21
22 # Count how many ugly numbers are <= mid using inclusion-exclusion principle
23 # Numbers divisible by a, b, or c
24 count = (mid // a + mid // b + mid // c
25 - mid // lcm_ab - mid // lcm_bc - mid // lcm_ac # Remove double counted
26 + mid // lcm_abc) # Add back triple counted
27
28 # If we have at least n ugly numbers <= mid, search in left half
29 if count >= n:
30 right = mid
31 else:
32 # Otherwise, search in right half
33 left = mid + 1
34
35 return left
36
1class Solution {
2 /**
3 * Find the nth ugly number that is divisible by a, b, or c.
4 * Uses binary search combined with inclusion-exclusion principle.
5 *
6 * @param n The position of the ugly number to find (1-indexed)
7 * @param a First divisor
8 * @param b Second divisor
9 * @param c Third divisor
10 * @return The nth ugly number divisible by a, b, or c
11 */
12 public int nthUglyNumber(int n, int a, int b, int c) {
13 // Calculate LCM for all pairs and triplet to apply inclusion-exclusion principle
14 long lcmAB = lcm(a, b);
15 long lcmBC = lcm(b, c);
16 long lcmAC = lcm(a, c);
17 long lcmABC = lcm(lcmAB, c);
18
19 // Binary search range: minimum is 1, maximum is 2 * 10^9
20 long left = 1;
21 long right = 2000000000;
22
23 // Binary search to find the nth ugly number
24 while (left < right) {
25 long mid = (left + right) >> 1; // Equivalent to (left + right) / 2
26
27 // Count how many ugly numbers are <= mid using inclusion-exclusion principle
28 // Count = |A| + |B| + |C| - |A∩B| - |B∩C| - |A∩C| + |A∩B∩C|
29 long count = mid / a + mid / b + mid / c
30 - mid / lcmAB - mid / lcmBC - mid / lcmAC
31 + mid / lcmABC;
32
33 // If count >= n, the answer is at mid or smaller
34 if (count >= n) {
35 right = mid;
36 } else {
37 // Otherwise, search in the upper half
38 left = mid + 1;
39 }
40 }
41
42 return (int) left;
43 }
44
45 /**
46 * Calculate Greatest Common Divisor using Euclidean algorithm.
47 *
48 * @param a First number
49 * @param b Second number
50 * @return GCD of a and b
51 */
52 private long gcd(long a, long b) {
53 return b == 0 ? a : gcd(b, a % b);
54 }
55
56 /**
57 * Calculate Least Common Multiple using the formula: LCM(a,b) = a*b / GCD(a,b).
58 *
59 * @param a First number
60 * @param b Second number
61 * @return LCM of a and b
62 */
63 private long lcm(long a, long b) {
64 return a * b / gcd(a, b);
65 }
66}
67
1class Solution {
2public:
3 int nthUglyNumber(int n, int a, int b, int c) {
4 // Calculate LCM for all pairs and triplet using inclusion-exclusion principle
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 // Binary search range: [1, 2*10^9]
11 long long left = 1;
12 long long right = 2000000000;
13
14 // Binary search to find the nth ugly number
15 while (left < right) {
16 long long mid = (left + right) >> 1;
17
18 // Count ugly numbers <= mid using inclusion-exclusion principle:
19 // |A ∪ B ∪ C| = |A| + |B| + |C| - |A ∩ B| - |B ∩ C| - |A ∩ C| + |A ∩ B ∩ C|
20 long long count = mid / a + mid / b + mid / c
21 - mid / lcm_ab - mid / lcm_bc - mid / lcm_ac
22 + mid / lcm_abc;
23
24 // If count >= n, the answer is in [left, mid]
25 if (count >= n) {
26 right = mid;
27 } else {
28 // Otherwise, the answer is in [mid+1, right]
29 left = mid + 1;
30 }
31 }
32
33 return left;
34 }
35
36private:
37 // Calculate Least Common Multiple of two numbers
38 long long calculateLCM(long long a, long long b) {
39 return a * b / calculateGCD(a, b);
40 }
41
42 // Calculate Greatest Common Divisor using Euclidean algorithm
43 long long calculateGCD(long long a, long long b) {
44 return b == 0 ? a : calculateGCD(b, a % b);
45 }
46};
47
1/**
2 * Finds the nth ugly number that is divisible by a, b, or c
3 * Uses binary search combined with inclusion-exclusion principle
4 * @param n - The position of the ugly number to find
5 * @param a - First divisor
6 * @param b - Second divisor
7 * @param c - Third divisor
8 * @returns The nth ugly number
9 */
10function nthUglyNumber(n: number, a: number, b: number, c: number): number {
11 // Calculate least common multiples for pairs and triplet
12 // These are needed for the inclusion-exclusion principle
13 const lcmAB = lcm(BigInt(a), BigInt(b));
14 const lcmBC = lcm(BigInt(b), BigInt(c));
15 const lcmAC = lcm(BigInt(a), BigInt(c));
16 const lcmABC = lcm(BigInt(a), lcmBC);
17
18 // Binary search boundaries
19 let left = 1n;
20 let right = BigInt(2e9);
21
22 // Binary search to find the nth ugly number
23 while (left < right) {
24 const mid = (left + right) >> 1n;
25
26 // Count how many ugly numbers are <= mid using inclusion-exclusion
27 // Add numbers divisible by a, b, c
28 // Subtract overlaps (divisible by pairs)
29 // Add back triple overlap (divisible by all three)
30 const count =
31 mid / BigInt(a) +
32 mid / BigInt(b) +
33 mid / BigInt(c) -
34 mid / lcmAB -
35 mid / lcmBC -
36 mid / lcmAC +
37 mid / lcmABC;
38
39 // Adjust search range based on count
40 if (count >= BigInt(n)) {
41 right = mid;
42 } else {
43 left = mid + 1n;
44 }
45 }
46
47 return Number(left);
48}
49
50/**
51 * Calculates the greatest common divisor using Euclidean algorithm
52 * @param a - First number
53 * @param b - Second number
54 * @returns The greatest common divisor of a and b
55 */
56function gcd(a: bigint, b: bigint): bigint {
57 return b === 0n ? a : gcd(b, a % b);
58}
59
60/**
61 * Calculates the least common multiple of two numbers
62 * @param a - First number
63 * @param b - Second number
64 * @returns The least common multiple of a and b
65 */
66function lcm(a: bigint, b: bigint): bigint {
67 return (a * b) / gcd(a, b);
68}
69
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. 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 even in languages with large integer support, or cause precision issues.
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
2. Incorrect Order of LCM Calculations for Three Numbers
When calculating lcm(a, b, c)
, a common mistake is treating it as lcm(a, lcm(b, c))
or 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 lcm_abc = lcm(a, b * c) # Conceptually wrong
Solution:
# Correct - LCM is associative lcm_abc = lcm(lcm(a, b), c) # Or equivalently lcm_abc = lcm(a, lcm(b, c))
3. Off-by-One Error in Binary Search Boundary
Setting the wrong condition for the binary search loop or incorrect boundary updates can lead to infinite loops or wrong answers.
Problem Example:
# Incorrect - might miss the answer while left <= right: # Using <= instead of < mid = (left + right) // 2 if count >= n: right = mid - 1 # Wrong: excludes potential answer else: left = mid + 1
Solution:
# Correct - maintains answer in [left, right] while left < right: mid = (left + right) // 2 if count >= n: right = mid # Keep mid as potential answer else: left = mid + 1
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.
Problem Example:
# If a = 2, b = 4, c = 6 # lcm(2, 4) = 4, not 8 # Need to ensure correct LCM calculation
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.
Problem Example:
# Incorrect - might be too small for large n
right = n * max(a, b, c) # Could underestimate for certain inputs
Solution:
# Safe upper bound per problem constraints right = 2 * 10**9
In a binary min heap, the minimum element can be found in:
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Want a Structured Path to Master System Design Too? Don’t Miss This!