313. Super Ugly Number
Problem Description
A super ugly number is a positive integer whose only prime factors come from a given array primes
.
Given an integer n
and an array of integers primes
, you need to find the n
th super ugly number in the sequence.
For example, if primes = [2, 3, 5]
, the sequence of super ugly numbers would be: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12, ...
because:
1
has no prime factors2 = 2
(prime factor: 2)3 = 3
(prime factor: 3)4 = 2 × 2
(prime factor: 2)5 = 5
(prime factor: 5)6 = 2 × 3
(prime factors: 2, 3)8 = 2 × 2 × 2
(prime factor: 2)- And so on...
The number 7
would not be in this sequence because 7
is a prime number not in the primes
array.
The problem guarantees that the n
th super ugly number will fit within a 32-bit signed integer (won't exceed 2^31 - 1
).
Intuition
To generate super ugly numbers, we need to think about how they're formed. Every super ugly number (except 1) is created by multiplying a smaller super ugly number by one of the primes in our array.
Starting with 1
, we can generate new super ugly numbers by multiplying it with each prime: 1 × 2
, 1 × 3
, 1 × 5
, etc. Then from these results, we can generate even more by multiplying them with the primes again.
The key insight is that we need to generate these numbers in sorted order to find the n
th one. A min heap is perfect for this - it always gives us the smallest unprocessed number. We start with 1
in the heap, then repeatedly:
- Extract the smallest number
x
from the heap - Multiply
x
by each prime to generate new candidates - Add these new candidates to the heap
However, this approach would generate duplicates. For instance, 6
could be generated as both 2 × 3
and 3 × 2
. To avoid duplicates, we use a clever optimization: when we extract a number x
, we only multiply it by primes that are greater than or equal to its smallest prime factor. This is achieved by breaking the loop when x % prime == 0
- if x
is divisible by a prime, we know that prime is its smallest prime factor, so we don't need to multiply by larger primes (they would create duplicates later).
The overflow check x <= mx // k
ensures we don't exceed the 32-bit integer limit when computing k * x
.
After extracting exactly n
numbers from the heap, the last extracted number is our answer - the n
th super ugly number.
Solution Approach
The solution uses a min heap (priority queue) to generate super ugly numbers in ascending order.
Implementation Steps:
-
Initialize the heap: Create a min heap
q
and add the first super ugly number1
to it. -
Set up variables:
x = 0
to store the current super ugly number being processedmx = (1 << 31) - 1
which equals2^31 - 1
, the maximum value for a 32-bit signed integer
-
Generate n super ugly numbers: Loop
n
times, and in each iteration:a. Extract minimum: Pop the smallest number from the heap using
heappop(q)
and store it inx
. This is guaranteed to be the next super ugly number in sequence.b. Generate new candidates: For each prime
k
in theprimes
array:- Overflow check: Before multiplying, verify that
x <= mx // k
to ensurek * x
won't overflow - Add to heap: If no overflow, push
k * x
into the heap as a new candidate - Duplicate prevention: If
x % k == 0
, it meansk
is the smallest prime factor ofx
. Break the loop to avoid generating duplicates (this implements the Euler sieve optimization)
- Overflow check: Before multiplying, verify that
-
Return result: After
n
iterations,x
contains then
th super ugly number.
Why the duplicate prevention works:
When x
is divisible by prime k
, we know that x = k × y
for some smaller super ugly number y
. If we continued with larger primes p > k
, we'd generate x × p = k × y × p
. But this same number will be generated later as y × p × k
when we process y × p
. By breaking early, we ensure each super ugly number is generated exactly once, following a canonical ordering where we multiply by primes in non-decreasing order.
Time Complexity: O(n × m × log(nm))
where m
is the size of the primes array, since we perform up to n × m
heap operations, each taking O(log(heap_size))
time.
Space Complexity: O(n × m)
for storing the heap, which can grow up to this size in the worst case.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through finding the 4th super ugly number with primes = [2, 3, 5]
.
Initial Setup:
- Heap:
[1]
- n = 4 (we need to extract 4 numbers)
- mx = 2^31 - 1 (overflow limit)
Iteration 1 (n=1):
- Extract minimum:
x = 1
(heap becomes empty) - Generate new candidates from
x = 1
:- For prime 2: Check
1 <= mx // 2
✓, add1 × 2 = 2
to heap - For prime 3: Check
1 <= mx // 3
✓, add1 × 3 = 3
to heap - For prime 5: Check
1 <= mx // 5
✓, add1 × 5 = 5
to heap
- For prime 2: Check
- Heap after iteration:
[2, 3, 5]
- 1st super ugly number: 1
Iteration 2 (n=2):
- Extract minimum:
x = 2
(heap becomes[3, 5]
) - Generate new candidates from
x = 2
:- For prime 2: Check
2 <= mx // 2
✓, add2 × 2 = 4
to heap - Check
2 % 2 == 0
✓, so break (avoid duplicates)
- For prime 2: Check
- Heap after iteration:
[3, 5, 4]
→[3, 4, 5]
(after heapify) - 2nd super ugly number: 2
Iteration 3 (n=3):
- Extract minimum:
x = 3
(heap becomes[4, 5]
) - Generate new candidates from
x = 3
:- For prime 2: Check
3 <= mx // 2
✓, add3 × 2 = 6
to heap - For prime 3: Check
3 <= mx // 3
✓, add3 × 3 = 9
to heap - Check
3 % 3 == 0
✓, so break (avoid duplicates)
- For prime 2: Check
- Heap after iteration:
[4, 5, 6, 9]
- 3rd super ugly number: 3
Iteration 4 (n=4):
- Extract minimum:
x = 4
(heap becomes[5, 6, 9]
) - Generate new candidates from
x = 4
:- For prime 2: Check
4 <= mx // 2
✓, add4 × 2 = 8
to heap - Check
4 % 2 == 0
✓, so break (avoid duplicates)
- For prime 2: Check
- Heap after iteration:
[5, 6, 9, 8]
→[5, 6, 8, 9]
(after heapify) - 4th super ugly number: 4
Result: The 4th super ugly number is 4.
Why duplicate prevention works: Notice when we extracted 4
, we only multiplied by 2
and stopped. If we had continued and multiplied 4
by 3
to get 12
, this would be a duplicate because 12
will also be generated as 6 × 2
when we process 6
later. By breaking when x % prime == 0
, we ensure each number is generated exactly once through a canonical path where we multiply by primes in non-decreasing order.
Solution Implementation
1from typing import List
2import heapq
3
4class Solution:
5 def nthSuperUglyNumber(self, n: int, primes: List[int]) -> int:
6 # Min-heap to store candidates for super ugly numbers
7 min_heap = [1]
8
9 # Current super ugly number
10 current_ugly = 0
11
12 # Maximum value to prevent integer overflow (2^31 - 1)
13 max_value = (1 << 31) - 1
14
15 # Generate n super ugly numbers
16 for _ in range(n):
17 # Extract the smallest number from heap (next super ugly number)
18 current_ugly = heapq.heappop(min_heap)
19
20 # Generate new candidates by multiplying current number with each prime
21 for prime in primes:
22 # Check for potential overflow before multiplication
23 if current_ugly <= max_value // prime:
24 heapq.heappush(min_heap, prime * current_ugly)
25
26 # Optimization: if current number is divisible by this prime,
27 # skip remaining primes to avoid generating duplicates
28 # (since smaller primes have already generated those multiples)
29 if current_ugly % prime == 0:
30 break
31
32 return current_ugly
33
1class Solution {
2 public int nthSuperUglyNumber(int n, int[] primes) {
3 // Min heap to store and retrieve the smallest super ugly number
4 PriorityQueue<Integer> minHeap = new PriorityQueue<>();
5
6 // Start with 1 as the first super ugly number
7 minHeap.offer(1);
8
9 int currentNumber = 0;
10
11 // Generate n super ugly numbers
12 while (n-- > 0) {
13 // Extract the smallest number from the heap
14 currentNumber = minHeap.poll();
15
16 // Remove duplicates by polling all equal values
17 while (!minHeap.isEmpty() && minHeap.peek() == currentNumber) {
18 minHeap.poll();
19 }
20
21 // Generate new super ugly numbers by multiplying current with each prime
22 for (int prime : primes) {
23 // Check for overflow before multiplication
24 if (prime <= Integer.MAX_VALUE / currentNumber) {
25 minHeap.offer(prime * currentNumber);
26 }
27
28 // Optimization: if current number is divisible by this prime,
29 // skip remaining primes to avoid generating redundant numbers
30 if (currentNumber % prime == 0) {
31 break;
32 }
33 }
34 }
35
36 return currentNumber;
37 }
38}
39
1class Solution {
2public:
3 int nthSuperUglyNumber(int n, vector<int>& primes) {
4 // Min heap to store super ugly numbers in ascending order
5 priority_queue<int, vector<int>, greater<int>> minHeap;
6
7 // Initialize with 1 (the first super ugly number)
8 minHeap.push(1);
9
10 int currentNumber = 0;
11
12 // Extract n super ugly numbers
13 while (n--) {
14 // Get the smallest super ugly number from heap
15 currentNumber = minHeap.top();
16 minHeap.pop();
17
18 // Generate new super ugly numbers by multiplying with each prime
19 for (int& prime : primes) {
20 // Check for overflow before multiplication
21 if (currentNumber <= INT_MAX / prime) {
22 minHeap.push(prime * currentNumber);
23 }
24
25 // Optimization: if current number is divisible by this prime,
26 // skip remaining primes to avoid generating duplicates
27 // (ensures each number is generated only once)
28 if (currentNumber % prime == 0) {
29 break;
30 }
31 }
32 }
33
34 return currentNumber;
35 }
36};
37
1function nthSuperUglyNumber(n: number, primes: number[]): number {
2 // Min heap to store super ugly numbers in ascending order
3 // Using array with custom comparison for min heap behavior
4 const minHeap: number[] = [];
5
6 // Helper function to insert element maintaining min heap property
7 const heapPush = (value: number): void => {
8 minHeap.push(value);
9 minHeap.sort((a, b) => a - b);
10 };
11
12 // Helper function to extract minimum element from heap
13 const heapPop = (): number => {
14 return minHeap.shift()!;
15 };
16
17 // Initialize with 1 (the first super ugly number)
18 heapPush(1);
19
20 let currentNumber: number = 0;
21
22 // Extract n super ugly numbers
23 while (n-- > 0) {
24 // Get the smallest super ugly number from heap
25 currentNumber = heapPop();
26
27 // Generate new super ugly numbers by multiplying with each prime
28 for (const prime of primes) {
29 // Check for overflow before multiplication
30 // JavaScript's MAX_SAFE_INTEGER ensures safe integer operations
31 if (currentNumber <= Number.MAX_SAFE_INTEGER / prime) {
32 heapPush(prime * currentNumber);
33 }
34
35 // Optimization: if current number is divisible by this prime,
36 // skip remaining primes to avoid generating duplicates
37 // (ensures each number is generated only once)
38 if (currentNumber % prime === 0) {
39 break;
40 }
41 }
42 }
43
44 return currentNumber;
45}
46
Time and Space Complexity
Time Complexity: O(n × m × log(n × m))
The algorithm uses a min-heap to generate the nth super ugly number. For each of the n
iterations:
- We perform one
heappop
operation:O(log(heap_size))
- We iterate through all
m
primes, and for each prime that doesn't cause early termination, we perform aheappush
operation:O(log(heap_size))
- In the worst case, we push all
m
primes into the heap for each number generated
The heap size can grow up to O(n × m)
in the worst case, as we might push m
elements per iteration and pop only 1. Therefore, each heap operation costs O(log(n × m))
.
Since we perform n
iterations, and each iteration involves up to m
heap push operations plus one pop operation, the total time complexity is O(n × m × log(n × m))
.
Space Complexity: O(n × m)
The space is dominated by the heap q
. In the worst-case scenario, the heap can contain up to O(n × m)
elements. This happens when we continuously add new numbers to the heap faster than we remove them. Since we add up to m
numbers per iteration and remove only 1, after n
iterations, the heap size can be bounded by O(n × m)
.
Common Pitfalls
1. Duplicate Generation Without Proper Prevention
The most critical pitfall is generating duplicate super ugly numbers in the heap, which leads to incorrect results and inefficiency.
Problem Example:
Without the if current_ugly % prime == 0: break
optimization, consider primes = [2, 3]
and current_ugly = 6
:
- When processing
6
, we'd generate6 × 2 = 12
and6 × 3 = 18
- Later, when processing
4
, we'd generate4 × 3 = 12
(duplicate!) - This creates duplicates in our sequence
Solution:
The break statement ensures we only multiply by primes in a canonical order. When current_ugly
is divisible by a prime, we stop there because any larger prime multiplication will be handled when we process the factored-out version later.
2. Integer Overflow
Without the overflow check if current_ugly <= max_value // prime
, multiplying large super ugly numbers with primes could cause integer overflow, leading to negative numbers or wrapped values in the heap.
Problem Example:
If current_ugly = 1,500,000,000
and prime = 3
, then current_ugly × prime = 4,500,000,000
which exceeds 2^31 - 1
.
Solution:
Always verify current_ugly <= max_value // prime
before multiplication. This ensures the product stays within the 32-bit signed integer range.
3. Using a Set Instead of Heap Optimization
A naive approach might use a set to track seen numbers to avoid duplicates:
seen = set()
if prime * current_ugly not in seen:
heapq.heappush(min_heap, prime * current_ugly)
seen.add(prime * current_ugly)
Problem: This uses O(n × m) extra space for the set and makes operations slower due to set lookups.
Solution: The break optimization elegantly prevents duplicates without extra space by ensuring each number is generated through exactly one path (its canonical factorization order).
4. Not Understanding Why the Break Works
Developers might remove the break thinking it's just an optimization, not realizing it's essential for correctness.
Key Insight:
The break ensures we only generate multiples using the "leftmost" prime factor path. For example, 12 = 2² × 3
is only generated as 4 × 3
, never as 6 × 2
, because when we process 6 = 2 × 3
, we break at prime 2
.
How many ways can you arrange the three letters A, B and C?
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
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!