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, orcindividually - 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
xis 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
aandb, bothaandc, or bothbandc - 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 bothaandbbc = lcm(b, c)- for numbers divisible by bothbandcac = lcm(a, c)- for numbers divisible by bothaandcabc = 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 smallestxsuch that there are at leastnugly numbers ≤x first_true_indextracks the smallest value found so far wherefeasible(mid)is true- When
feasible(mid)is true, we savemidas 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 <= rightto 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 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) = 6lcm(2, 5) = 10lcm(3, 5) = 15lcm(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 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
371class 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}
531class 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};
471function 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}
48Time 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,abcfor the least common multiples - Variables
l,r,midfor 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
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
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
https assets algo monster cover_photos Binary_Search svg Binary Search Intuition Binary search is an efficient array search algorithm It works by narrowing down the search range by half each time If you have looked up a word in a physical dictionary you've already used binary search in real life Let's
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!