277. Ugly Number II
๐ LeetCode Problem: 264. Ugly Number II
๐ Difficulty: Medium
๐ท๏ธ Topics: Hash Table, Math, Dynamic Programming, Heap (Priority Queue)
Problem Statement
An ugly number is a positive integer whose prime factors are limited to 2, 3, and 5.
Given an integer n, return the nth ugly number.
Example 1:
Input: n = 10
Output: 12
Explanation: [1, 2, 3, 4, 5, 6, 8, 9, 10, 12] is the sequence of the first 10 ugly numbers.
Example 2:
Input: n = 1
Output: 1
Explanation: 1 has no prime factors, therefore all of its prime factors are limited to 2, 3, and 5.
Constraints:
- 1 <= n <= 1690
๐ Understanding Ugly Numbers
What are ugly numbers?
Ugly numbers: Only have prime factors 2, 3, and 5
First 15 ugly numbers:
1 = 1
2 = 2
3 = 3
4 = 2 ร 2
5 = 5
6 = 2 ร 3
8 = 2 ร 2 ร 2
9 = 3 ร 3
10 = 2 ร 5
12 = 2 ร 2 ร 3
15 = 3 ร 5
16 = 2 ร 2 ร 2 ร 2
18 = 2 ร 3 ร 3
20 = 2 ร 2 ร 5
24 = 2 ร 2 ร 2 ร 3
NOT ugly numbers:
7 = 7 (has prime factor 7)
11 = 11 (has prime factor 11)
14 = 2 ร 7 (has prime factor 7)
Key Observation:
CRITICAL INSIGHT:
Every ugly number (except 1) is formed by:
- Taking a previous ugly number
- Multiplying by 2, 3, or 5
Example:
1 is ugly
1 ร 2 = 2 (ugly)
1 ร 3 = 3 (ugly)
2 ร 2 = 4 (ugly)
2 ร 3 = 6 (ugly)
3 ร 2 = 6 (ugly) - duplicate!
Each ugly number = previous ugly ร {2, 3, or 5}
This is the KEY to solving efficiently!
๐ The Natural Thinking Process
When you first see this problem:
Question: Find the nth ugly number
First Thought: "Check each number starting from 1"
- For each number, check if it's ugly
- Count ugly numbers
- Stop when count = n
Is this optimal? NO, but it WORKS!
Let's try it first.
The Evolution:
BRUTE FORCE THINKING:
"Check 1, 2, 3, 4, 5, 6, 7, 8..."
"For each, divide by 2,3,5 repeatedly"
Time: O(n ร log m) where m is the nth ugly number
Problem: m can be VERY large!
โฌ
REALIZATION 1:
"I'm checking NON-ugly numbers too (7, 11, 13...)"
"Waste of time!"
"What if I GENERATE only ugly numbers?"
โฌ
OPTIMIZATION 1: Min-Heap
"Generate candidates by multiplying previous ugly by 2,3,5"
"Use min-heap to get smallest candidate"
Time: O(n log n) - better!
But uses O(n) space for heap
โฌ
REALIZATION 2:
"I'm generating same number multiple ways (duplicates!)"
"Need to track what I've seen"
"What if I track THREE separate sequences?"
โฌ
OPTIMIZATION 2: Three Pointers (DP)
"Maintain three pointers for ร2, ร3, ร5"
"Always pick minimum"
Time: O(n), Space: O(n) - OPTIMAL!
๐ฏ Approach 1: Brute Force - Check Each Number โญ
The First Natural Solution
The Thought Process:
Step 1: Read problem
"Find nth ugly number"
Step 2: How to check if ugly?
"Divide by 2 while divisible"
"Divide by 3 while divisible"
"Divide by 5 while divisible"
"If result is 1 โ ugly!"
Step 3: Generate sequence
"Start from 1, check each number"
"Count ugly numbers until count = n"
This is slow for large n, but let's code it!
Helper: Is Number Ugly?
private boolean isUgly(int num) {
if (num <= 0) return false;
// Divide by 2 while possible
while (num % 2 == 0) {
num /= 2;
}
// Divide by 3 while possible
while (num % 3 == 0) {
num /= 3;
}
// Divide by 5 while possible
while (num % 5 == 0) {
num /= 5;
}
// If only factors were 2,3,5 โ result is 1
return num == 1;
}
Visual Tracking - Find 10th Ugly Number
Goal: Find 10th ugly number
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
CHECK NUMBERS ONE BY ONE
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
num=1: Is 1 ugly?
No factors to divide
Result: 1 โ YES, ugly!
Count: 1
num=2: Is 2 ugly?
2 รท 2 = 1
Result: 1 โ YES, ugly!
Count: 2
num=3: Is 3 ugly?
3 รท 3 = 1
Result: 1 โ YES, ugly!
Count: 3
num=4: Is 4 ugly?
4 รท 2 = 2 รท 2 = 1
Result: 1 โ YES, ugly!
Count: 4
num=5: Is 5 ugly?
5 รท 5 = 1
Result: 1 โ YES, ugly!
Count: 5
num=6: Is 6 ugly?
6 รท 2 = 3 รท 3 = 1
Result: 1 โ YES, ugly!
Count: 6
num=7: Is 7 ugly?
Can't divide by 2,3,5
Result: 7 โ NO, not ugly! โ
Count: 6 (unchanged)
num=8: Is 8 ugly?
8 รท 2 = 4 รท 2 = 2 รท 2 = 1
Result: 1 โ YES, ugly!
Count: 7
num=9: Is 9 ugly?
9 รท 3 = 3 รท 3 = 1
Result: 1 โ YES, ugly!
Count: 8
num=10: Is 10 ugly?
10 รท 2 = 5 รท 5 = 1
Result: 1 โ YES, ugly!
Count: 9
num=11: Is 11 ugly?
Can't divide by 2,3,5
Result: 11 โ NO, not ugly! โ
Count: 9 (unchanged)
num=12: Is 12 ugly?
12 รท 2 = 6 รท 2 = 3 รท 3 = 1
Result: 1 โ YES, ugly!
Count: 10 โ REACHED n=10!
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
RESULT
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
10th ugly number: 12 โ
Ugly numbers found: [1, 2, 3, 4, 5, 6, 8, 9, 10, 12]
Numbers checked: 12
Wasted checks: 7, 11 (not ugly)
Implementation
/**
* Brute Force: Check each number
* Time: O(n ร log m) where m is the nth ugly number
* Space: O(1)
*/
class Solution {
public int nthUglyNumber(int n) {
int count = 0;
int num = 1;
while (count < n) {
if (isUgly(num)) {
count++;
}
if (count < n) {
num++;
}
}
return num;
}
private boolean isUgly(int num) {
if (num <= 0) return false;
while (num % 2 == 0) num /= 2;
while (num % 3 == 0) num /= 3;
while (num % 5 == 0) num /= 5;
return num == 1;
}
}
โฐ Time: O(n ร log m) - Check n ugly numbers, each check O(log m)
๐พ Space: O(1)
Why This is Slow:
Problem: We check MANY non-ugly numbers!
For n=10: Check 12 numbers (2 wasted)
For n=100: Check ~200 numbers (100 wasted)
For n=1690: Check ~850,000 numbers! ๐ฑ
The 1690th ugly number is 2,123,366,400
We'd check 2 BILLION numbers!
Bottleneck: Checking non-ugly numbers is wasteful!
๐ก The First AHA Moment
PROBLEM ANALYSIS:
"I'm checking 7, 11, 13, 17, 19, 23..."
"None of these are ugly!"
"Complete waste of time!"
KEY INSIGHT:
"What if I ONLY GENERATE ugly numbers?"
Every ugly number = previous ugly ร {2, 3, or 5}
From 1: Generate 1ร2=2, 1ร3=3, 1ร5=5
From 2: Generate 2ร2=4, 2ร3=6, 2ร5=10
From 3: Generate 3ร2=6, 3ร3=9, 3ร5=15
...
ONLY generate ugly numbers!
Never check non-ugly!
How to pick next? Use MIN-HEAP!
๐ฏ Approach 2: Min-Heap - Generate Only Ugly Numbers โญโญ
The Better Solution
The Evolution of Thought:
Brute Force: Check every number (wasteful)
โฌ
Question: "Can I generate ONLY ugly numbers?"
โฌ
Answer: "YES! Each ugly = previous ugly ร {2,3,5}"
โฌ
Better Idea: "Use MIN-HEAP to get smallest candidate!"
Understanding the Generation Process
GENERATION STRATEGY:
Start with 1 (first ugly)
Generate candidates:
1 ร 2 = 2
1 ร 3 = 3
1 ร 5 = 5
Pick smallest: 2
Generate from 2:
2 ร 2 = 4
2 ร 3 = 6
2 ร 5 = 10
Current candidates: [3, 5, 4, 6, 10]
Pick smallest: 3
Generate from 3:
3 ร 2 = 6 (duplicate!)
3 ร 3 = 9
3 ร 5 = 15
Min-heap helps us:
1. Always get smallest candidate
2. Track what we've seen (avoid duplicates)
Visual Tracking - Find 10th Ugly Number
Goal: Find 10th ugly number using min-heap
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
INITIALIZATION
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Min-Heap: [1]
Seen: {1}
Ugly count: 0
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
STEP 1: Get 1st ugly number
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Poll from heap: 1
Ugly[1] = 1 โ
Generate candidates from 1:
1 ร 2 = 2 (not in seen)
1 ร 3 = 3 (not in seen)
1 ร 5 = 5 (not in seen)
Add to heap: [2, 3, 5]
Seen: {1, 2, 3, 5}
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
STEP 2: Get 2nd ugly number
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Heap: [2, 3, 5]
Poll smallest: 2
Ugly[2] = 2 โ
Generate from 2:
2 ร 2 = 4 (not in seen)
2 ร 3 = 6 (not in seen)
2 ร 5 = 10 (not in seen)
Add to heap: [3, 5, 4, 6, 10]
โ
min (heap property)
Seen: {1, 2, 3, 5, 4, 6, 10}
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
STEP 3: Get 3rd ugly number
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Heap: [3, 5, 4, 6, 10]
Poll smallest: 3
Ugly[3] = 3 โ
Generate from 3:
3 ร 2 = 6 (already in seen!) โ DUPLICATE
3 ร 3 = 9 (not in seen)
3 ร 5 = 15 (not in seen)
Add new ones: [4, 5, 6, 10, 9, 15]
Seen: {1, 2, 3, 4, 5, 6, 10, 9, 15}
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
STEP 4: Get 4th ugly number
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Heap: [4, 5, 6, 10, 9, 15]
Poll smallest: 4
Ugly[4] = 4 โ
Generate from 4:
4 ร 2 = 8 (not in seen)
4 ร 3 = 12 (not in seen)
4 ร 5 = 20 (not in seen)
Add to heap: [5, 6, 9, 10, 15, 8, 12, 20]
Seen: {..., 8, 12, 20}
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
STEP 5: Get 5th ugly number
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Heap: [5, 6, 9, 10, 15, 8, 12, 20]
Poll smallest: 5
Ugly[5] = 5 โ
Generate from 5:
5 ร 2 = 10 (already in seen!) โ DUPLICATE
5 ร 3 = 15 (already in seen!) โ DUPLICATE
5 ร 5 = 25 (not in seen)
Add new: [6, 8, 9, 10, 12, 15, 20, 25]
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
STEP 6: Get 6th ugly number
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Heap: [6, 8, 9, 10, 12, 15, 20, 25]
Poll smallest: 6
Ugly[6] = 6 โ
Generate from 6:
6 ร 2 = 12 (duplicate)
6 ร 3 = 18 (new)
6 ร 5 = 30 (new)
Add: [8, 9, 10, 12, 15, 20, 25, 18, 30]
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
STEP 7-10: Continue similarly
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Step 7: Poll 8 โ Ugly[7] = 8
Step 8: Poll 9 โ Ugly[8] = 9
Step 9: Poll 10 โ Ugly[9] = 10
Step 10: Poll 12 โ Ugly[10] = 12 โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
RESULT
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
10th ugly number: 12 โ
Sequence: [1, 2, 3, 4, 5, 6, 8, 9, 10, 12]
Key Observations:
โ Only generated ugly numbers (no 7, 11, etc.)
โ Used min-heap to always get smallest
โ Used HashSet to avoid duplicates
โ Much faster than brute force!
Implementation
import java.util.*;
/**
* Min-Heap with generation
* Time: O(n log n)
* Space: O(n)
*/
class Solution {
public int nthUglyNumber(int n) {
// Min-heap to get smallest candidate
PriorityQueue<Long> minHeap = new PriorityQueue<>();
// HashSet to avoid duplicates
Set<Long> seen = new HashSet<>();
// Start with 1
minHeap.offer(1L);
seen.add(1L);
long ugly = 1L;
for (int i = 0; i < n; i++) {
// Get smallest candidate
ugly = minHeap.poll();
// Generate new candidates
for (int factor : new int[]{2, 3, 5}) {
long newUgly = ugly * factor;
if (!seen.contains(newUgly)) {
minHeap.offer(newUgly);
seen.add(newUgly);
}
}
}
return (int) ugly;
}
}
โฐ Time: O(n log n) - n iterations, each with heap operations
๐พ Space: O(n) - Heap and HashSet storage
Why This is Better:
โ Only generates ugly numbers (no wasted checks!)
โ Uses min-heap to get smallest efficiently
โ HashSet prevents duplicates
But... can we do better?
- O(n log n) is good
- But O(n) space for heap + set
- Can we achieve O(n) time with less overhead?
๐ก The Second AHA Moment
PROBLEM ANALYSIS:
Min-Heap approach generates candidates:
From 1: 2, 3, 5
From 2: 4, 6, 10
From 3: 6 (dup!), 9, 15
...
OBSERVATION:
"I'm generating duplicates!"
"6 comes from both 2ร3 and 3ร2"
"10 comes from both 2ร5 and 5ร2"
KEY INSIGHT:
"What if I track THREE separate sequences?"
Sequence ร2: 1ร2, 2ร2, 3ร2, 4ร2, 5ร2...
Sequence ร3: 1ร3, 2ร3, 3ร3, 4ร3, 5ร3...
Sequence ร5: 1ร5, 2ร5, 3ร5, 4ร5, 5ร5...
At each step:
- Pick minimum of three
- Advance pointer that gave minimum
- Automatically handles duplicates!
This is DYNAMIC PROGRAMMING! ๐ฏ
๐ฏ Approach 3: Three Pointers DP (Optimal) โญโญโญ
The Optimal Solution
The Evolution of Thought:
Brute Force: Check all numbers
โฌ
Min-Heap: Generate only ugly
โฌ
Question: "Can I avoid duplicates altogether?"
โฌ
Answer: "Track three sequences with pointers!"
โฌ
Optimal: Three pointers DP - O(n) time, O(n) space
Understanding Three Pointers
THE BRILLIANT INSIGHT:
Maintain array: ugly[1..n]
Maintain three pointers: p2, p3, p5
Next candidates:
ugly[p2] ร 2
ugly[p3] ร 3
ugly[p5] ร 5
Pick minimum of three โ that's next ugly number
Advance pointer(s) that gave minimum
WHY Advance Pointer That Gave Minimum? - The Critical Reasoning
The Core Question:
We have: ugly[p2]ร2, ugly[p3]ร3, ugly[p5]ร5
We picked minimum and added to array.
Question: Which pointer(s) should we advance? Why?
Answer Through Reasoning:
PRINCIPLE: Each pointer represents a SEQUENCE
Sequence from ร2: [1ร2, 2ร2, 3ร2, 4ร2, 5ร2, 6ร2, ...]
[2, 4, 6, 8, 10, 12, ...]
โ
p2 points here initially
Sequence from ร3: [1ร3, 2ร3, 3ร3, 4ร3, 5ร3, 6ร3, ...]
[3, 6, 9, 12, 15, 18, ...]
โ
p3 points here initially
Sequence from ร5: [1ร5, 2ร5, 3ร5, 4ร5, 5ร5, 6ร5, ...]
[5, 10, 15, 20, 25, 30, ...]
โ
p5 points here initially
GOAL: Merge these three sequences in sorted order!
Think of it like merging three sorted lists!
Detailed Reasoning - Step by Step:
STEP 1: Initial state
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ugly = [1, ?, ?, ?, ...]
p2 = p3 = p5 = 0 (all point to ugly[0] = 1)
Sequences and what each pointer sees:
ร2 sequence: p2 โ ugly[0]ร2 = 1ร2 = 2, then 2ร2, 3ร2, ...
ร3 sequence: p3 โ ugly[0]ร3 = 1ร3 = 3, then 2ร3, 3ร3, ...
ร5 sequence: p5 โ ugly[0]ร5 = 1ร5 = 5, then 2ร5, 3ร5, ...
Candidates: [2, 3, 5]
Minimum: 2
ugly[1] = 2 โ
QUESTION: Which pointer to advance?
REASONING:
We just used "2" from the ร2 sequence
If we DON'T advance p2:
Next time, ร2 sequence gives: ugly[0]ร2 = 2 again!
We'd keep getting 2 forever! โ
If we DO advance p2:
Next time, ร2 sequence gives: ugly[1]ร2 = 2ร2 = 4 โ
We move forward in the ร2 sequence!
What about p3 and p5?
They didn't give minimum (3 and 5 are still candidates)
We haven't used them yet!
DON'T advance them - they're still valid candidates!
RULE: Advance the pointer whose sequence gave the minimum!
New state:
p2 = 1 (moved forward)
p3 = 0 (stayed - 3 still available)
p5 = 0 (stayed - 5 still available)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
STEP 2: After picking 2
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ugly = [1, 2, ?, ?, ...]
p2 = 1, p3 = 0, p5 = 0
Sequences now:
ร2 sequence: p2 โ ugly[1]ร2 = 2ร2 = 4
ร3 sequence: p3 โ ugly[0]ร3 = 1ร3 = 3 (still!)
ร5 sequence: p5 โ ugly[0]ร5 = 1ร5 = 5 (still!)
Candidates: [4, 3, 5]
Minimum: 3
ugly[2] = 3 โ
REASONING:
We used "3" from ร3 sequence
Advance p3 to get next value from ร3 sequence
DON'T advance p2 (didn't give min)
DON'T advance p5 (didn't give min)
New state:
p2 = 1 (stayed)
p3 = 1 (moved forward - we used this)
p5 = 0 (stayed)
What if We DON'T Follow This Rule?
COUNTER-EXAMPLE: What if we advance WRONG pointer?
State: ugly = [1, 2, ?, ...]
p2 = 1, p3 = 0, p5 = 0
Candidates: [4, 3, 5]
Minimum: 3
CORRECT: Advance p3
Result: [1, 2, 3, 4, 5, 6, ...] โ
WRONG: Advance p2 instead
Next candidates: ugly[2]ร2, ugly[0]ร3, ugly[0]ร5
= 3ร2, 1ร3, 1ร5
= 6, 3, 5
But ugly[2] = 3!
So we'd compute: 3ร2 = 6
Candidates: [6, 3, 5]
We'd pick 3 AGAIN! โ
Duplicate! Array becomes [1, 2, 3, 3, ...] โ
OR if we skip duplicates, we'd pick 5 next
Array becomes [1, 2, 3, 5, ...] โ
We MISSED 4! Not in sorted order!
WRONG: Advance p5 instead
Similar problems - wrong order!
CONCLUSION: MUST advance the pointer that gave minimum!
Special Case: Multiple Pointers Give Same Minimum (DUPLICATES)
EXAMPLE: When does this happen?
ugly = [1, 2, 3, 4, 5, ?, ...]
p2 = 2, p3 = 1, p5 = 0
Candidates:
ugly[2] ร 2 = 3 ร 2 = 6
ugly[1] ร 3 = 2 ร 3 = 6 โ SAME!
ugly[0] ร 5 = 1 ร 5 = 5
Minimum: 5 (from p5)
ugly[5] = 5, advance p5
p2 = 2, p3 = 1, p5 = 1
Next iteration:
Candidates:
ugly[2] ร 2 = 3 ร 2 = 6
ugly[1] ร 3 = 2 ร 3 = 6 โ BOTH give 6!
ugly[1] ร 5 = 2 ร 5 = 10
Minimum: 6
QUESTION: Should we advance just p2? Just p3? Or BOTH?
REASONING:
If we advance ONLY p2:
p2 = 3, p3 = 1, p5 = 1
Next candidates:
ugly[3] ร 2 = 4 ร 2 = 8
ugly[1] ร 3 = 2 ร 3 = 6 โ 6 AGAIN!
ugly[1] ร 5 = 2 ร 5 = 10
We'd pick 6 again! โ
Duplicate in array!
If we advance ONLY p3:
Same problem - p2 still gives 6!
If we advance BOTH p2 AND p3:
p2 = 3, p3 = 2, p5 = 1
Next candidates:
ugly[3] ร 2 = 4 ร 2 = 8
ugly[2] ร 3 = 3 ร 3 = 9
ugly[1] ร 5 = 2 ร 5 = 10
All different! โ
No duplicates!
RULE FOR DUPLICATES:
Advance ALL pointers that gave the minimum!
This ensures each sequence moves past that value!
CODE:
if (nextUgly == next2) p2++; // Not else-if!
if (nextUgly == next3) p3++; // Check all!
if (nextUgly == next5) p5++; // Advance all matches!
Visual Analogy - Merging Sorted Lists:
Think of three sorted lists being merged:
List A (ร2): [2, 4, 6, 8, 10, 12, ...]
โ
p2
List B (ร3): [3, 6, 9, 12, 15, 18, ...]
โ
p3
List C (ร5): [5, 10, 15, 20, 25, 30, ...]
โ
p5
To merge in sorted order:
Step 1: Compare heads: 2, 3, 5
Pick 2 (minimum)
Advance pointer in List A (we used it!)
List A: [2, 4, 6, 8, 10, 12, ...]
โ
p2
List B: [3, 6, 9, 12, 15, 18, ...]
โ
p3
List C: [5, 10, 15, 20, 25, 30, ...]
โ
p5
Step 2: Compare heads: 4, 3, 5
Pick 3 (minimum)
Advance pointer in List B
This is EXACTLY what we're doing!
Just that our "lists" are generated on-the-fly
using the ugly numbers we've already found!
GOLDEN RULE:
When you use an element from a sequence,
you MUST advance that sequence's pointer
to get the next element from it!
Otherwise, you'll keep getting the same element!
Summary - The Complete Logic:
WHY advance pointer that gave minimum?
1. PREVENTS STAGNATION:
Not advancing โ same value forever
2. MAINTAINS SORTED ORDER:
Each sequence is sorted
Picking minimum + advancing = merged sorted sequence
3. HANDLES DUPLICATES:
Multiple sequences can produce same value (6 = 2ร3 = 3ร2)
Advance ALL that match โ no duplicates
4. GENERATES ALL UGLY NUMBERS:
Each ugly appears in at least one sequence
By advancing after use, we eventually process all
5. OPTIMAL EFFICIENCY:
Each ugly number generated exactly once
O(n) time - can't be better!
This is the MAGIC of the three-pointer approach! โจ
Visual Tracking - Find First 10 Ugly Numbers
Goal: Generate first 10 ugly numbers using three pointers
ugly = [1, ?, ?, ?, ?, ?, ?, ?, ?, ?]
p2 = p3 = p5 = 0
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
STEP 1: Find ugly[1]
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ugly = [1, ?, ?, ?, ?, ?, ?, ?, ?, ?]
โ
p2,p3,p5
Candidates:
ugly[p2] ร 2 = ugly[0] ร 2 = 1 ร 2 = 2
ugly[p3] ร 3 = ugly[0] ร 3 = 1 ร 3 = 3
ugly[p5] ร 5 = ugly[0] ร 5 = 1 ร 5 = 5
Minimum: 2
ugly[1] = 2 โ
Advance p2 (it gave minimum)
ugly = [1, 2, ?, ?, ?, ?, ?, ?, ?, ?]
โ โ
p3,p5 p2
State: p2=1, p3=0, p5=0
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
STEP 2: Find ugly[2]
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ugly = [1, 2, ?, ?, ?, ?, ?, ?, ?, ?]
โ โ
p3,p5 p2
Candidates:
ugly[p2] ร 2 = ugly[1] ร 2 = 2 ร 2 = 4
ugly[p3] ร 3 = ugly[0] ร 3 = 1 ร 3 = 3
ugly[p5] ร 5 = ugly[0] ร 5 = 1 ร 5 = 5
Minimum: 3
ugly[2] = 3 โ
Advance p3
ugly = [1, 2, 3, ?, ?, ?, ?, ?, ?, ?]
โ โ
p5 p3
p2
State: p2=1, p3=1, p5=0
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
STEP 3: Find ugly[3]
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ugly = [1, 2, 3, ?, ?, ?, ?, ?, ?, ?]
Candidates:
ugly[1] ร 2 = 2 ร 2 = 4
ugly[1] ร 3 = 2 ร 3 = 6
ugly[0] ร 5 = 1 ร 5 = 5
Minimum: 4
ugly[3] = 4 โ
Advance p2
ugly = [1, 2, 3, 4, ?, ?, ?, ?, ?, ?]
โ โ โ
p5 p3 p2
State: p2=2, p3=1, p5=0
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
STEP 4: Find ugly[4]
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Candidates:
ugly[2] ร 2 = 3 ร 2 = 6
ugly[1] ร 3 = 2 ร 3 = 6
ugly[0] ร 5 = 1 ร 5 = 5
Minimum: 5
ugly[4] = 5 โ
Advance p5
State: p2=2, p3=1, p5=1
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
STEP 5: Find ugly[5]
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Candidates:
ugly[2] ร 2 = 3 ร 2 = 6
ugly[1] ร 3 = 2 ร 3 = 6
ugly[1] ร 5 = 2 ร 5 = 10
Minimum: 6
IMPORTANT: Both p2 and p3 give 6 (DUPLICATE!)
Solution: Advance BOTH p2 and p3!
ugly[5] = 6 โ
Advance p2 AND p3 (both gave 6)
State: p2=3, p3=2, p5=1
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
STEP 6: Find ugly[6]
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ugly = [1, 2, 3, 4, 5, 6, ?, ?, ?, ?]
Candidates:
ugly[3] ร 2 = 4 ร 2 = 8
ugly[2] ร 3 = 3 ร 3 = 9
ugly[1] ร 5 = 2 ร 5 = 10
Minimum: 8
ugly[6] = 8 โ
Advance p2
State: p2=4, p3=2, p5=1
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
STEP 7: Find ugly[7]
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Candidates:
ugly[4] ร 2 = 5 ร 2 = 10
ugly[2] ร 3 = 3 ร 3 = 9
ugly[1] ร 5 = 2 ร 5 = 10
Minimum: 9
ugly[7] = 9 โ
Advance p3
State: p2=4, p3=3, p5=1
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
STEP 8: Find ugly[8]
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Candidates:
ugly[4] ร 2 = 5 ร 2 = 10
ugly[3] ร 3 = 4 ร 3 = 12
ugly[1] ร 5 = 2 ร 5 = 10
Minimum: 10
Both p2 and p5 give 10!
Advance BOTH
ugly[8] = 10 โ
State: p2=5, p3=3, p5=2
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
STEP 9: Find ugly[9]
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Candidates:
ugly[5] ร 2 = 6 ร 2 = 12
ugly[3] ร 3 = 4 ร 3 = 12
ugly[2] ร 5 = 3 ร 5 = 15
Minimum: 12
Both p2 and p3 give 12!
Advance BOTH
ugly[9] = 12 โ
State: p2=6, p3=4, p5=2
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
RESULT
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ugly = [1, 2, 3, 4, 5, 6, 8, 9, 10, 12]
10th ugly number: ugly[9] = 12 โ
Key Insights:
โ Each pointer generates a sequence
โ Always pick minimum of three
โ Advance pointer(s) that gave minimum
โ Automatically handles duplicates!
โ No heap needed, no HashSet needed!
โ O(n) time, O(n) space - OPTIMAL!
Implementation
/**
* Three Pointers DP (Optimal)
* Time: O(n)
* Space: O(n)
*/
class Solution {
public int nthUglyNumber(int n) {
int[] ugly = new int[n];
ugly[0] = 1;
// Three pointers
int p2 = 0, p3 = 0, p5 = 0;
for (int i = 1; i < n; i++) {
// Calculate next candidates
int next2 = ugly[p2] * 2;
int next3 = ugly[p3] * 3;
int next5 = ugly[p5] * 5;
// Pick minimum
int nextUgly = Math.min(next2, Math.min(next3, next5));
ugly[i] = nextUgly;
// Advance pointer(s) that gave minimum
// This handles duplicates automatically!
if (nextUgly == next2) p2++;
if (nextUgly == next3) p3++;
if (nextUgly == next5) p5++;
}
return ugly[n - 1];
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.nthUglyNumber(10)); // 12
System.out.println(sol.nthUglyNumber(1)); // 1
}
}
โฐ Time: O(n) - Single pass through array
๐พ Space: O(n) - Array storage only
Why This is Optimal:
โ O(n) time - can't do better (must generate n numbers)
โ O(n) space - minimal (just the result array)
โ No heap overhead
โ No HashSet overhead
โ Automatically handles duplicates
โ Simple, elegant code
โ Perfect for interviews!
๐ Approach Comparison - The Complete Growth Journey
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโ
โ Approach โ Time โ Space โ Key Insight โ
โโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโค
โ Brute Force โ O(nรlog m) โ O(1) โ Check each number โ
โ Min-Heap โ O(n log n) โ O(n) โ Generate only ugly โ
โ Three Ptr DP โ O(n) โ O(n) โ Track 3 sequences โ
โโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโดโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโ
THE COMPLETE LEARNING PROGRESSION:
Level 1: Brute Force
Thought: "Check 1, 2, 3, 4... until count = n"
Works? YES โ
Optimal? NO โ
Problem: Checks many non-ugly numbers
For n=1690: Checks ~850 MILLION numbers! ๐ฑ
Level 2: Optimization Insight #1
Question: "Why check non-ugly numbers?"
Realization: "Every ugly = prev ugly ร {2,3,5}"
Idea: "GENERATE only ugly numbers!"
Level 3: Min-Heap Solution
Implementation: Use heap + HashSet
Result: O(n log n), O(n) space
Better: Only generates ugly numbers! โ
But: Heap overhead, duplicates need HashSet
Level 4: Optimization Insight #2
Question: "Can I avoid duplicates?"
Realization: "Track THREE sequences!"
Idea: "Use pointers, pick minimum!"
Level 5: Three Pointers DP
Implementation: Array + 3 pointers
Result: O(n) time, O(n) space
Perfect: Optimal complexity, simple code! โ
Growth: Learned DP pattern! ๐
CONCRETE EXAMPLE (n=1690):
Brute Force:
Check ~850,000,000 numbers
Each check: O(log m) divisions
Total: BILLIONS of operations! ๐ฑ
Min-Heap:
Generate 1690 numbers
Each: O(log 1690) โ 10 heap ops
Total: ~17,000 operations
500,000x FASTER! ๐
Three Pointers:
Generate 1690 numbers
Each: 3 comparisons + 1-3 increments
Total: ~6,700 operations
2.5x better than heap! โจ
๐ก Key Learnings - Your Complete Growth
WHAT YOU LEARNED:
1. BRUTE FORCE FIRST:
โ Natural: "Check each number"
โ Simple: Easy to code and verify
โ Valid: Correct but slow
โ Foundation: Build from here!
2. FIRST OPTIMIZATION (Generation):
โ Insight: "Don't check non-ugly!"
โ Strategy: "Generate only ugly"
โ Tool: Min-heap for smallest
โ Improvement: O(nรlog m) โ O(n log n)
3. SECOND OPTIMIZATION (DP):
โ Insight: "Avoid duplicates!"
โ Strategy: "Track three sequences"
โ Tool: Three pointers
โ Improvement: O(n log n) โ O(n)
โ Learned: DP pattern with pointers!
4. PATTERN RECOGNITION:
โ Multiple sequences merging
โ Pick minimum from candidates
โ Advance pointer(s) that gave min
โ This is a COMMON DP pattern!
INTERVIEW STRATEGY:
Progressive Presentation:
"I can start by checking each number, but that's slow.
Better: Generate only ugly numbers.
Each ugly = previous ร {2,3,5}.
Use min-heap to get smallest.
Time: O(n log n), Space: O(n).
Optimal: Track three sequences with pointers.
Always pick minimum of three.
Advance pointer(s) that gave minimum.
Time: O(n), Space: O(n).
Let me implement the DP solution."
Shows:
โ Problem-solving progression
โ Optimization thinking
โ Understanding of trade-offs
โ Knowledge of optimal solution
This is REAL growth! ๐ฑโ๐ณโ๐ฒ
โ ๏ธ Common Mistakes
Mistake 1: Not handling duplicates in DP
// โ WRONG - Only advances one pointer
if (nextUgly == next2) p2++;
else if (nextUgly == next3) p3++;
else if (nextUgly == next5) p5++;
// โ CORRECT - Advances ALL that match
if (nextUgly == next2) p2++;
if (nextUgly == next3) p3++;
if (nextUgly == next5) p5++;
Mistake 2: Using int instead of long in heap
// โ WRONG - Overflow for large ugly numbers
PriorityQueue<Integer> heap = new PriorityQueue<>();
// โ CORRECT - Use long to avoid overflow
PriorityQueue<Long> heap = new PriorityQueue<>();
Mistake 3: Wrong initialization in DP
// โ WRONG - All pointers start at different positions
int p2 = 0, p3 = 1, p5 = 2;
// โ CORRECT - All start at 0 (pointing to 1)
int p2 = 0, p3 = 0, p5 = 0;
Mistake 4: Not checking for duplicates in heap approach
// โ WRONG - Adds duplicates to heap
for (int factor : new int[]{2, 3, 5}) {
heap.offer(ugly * factor);
}
// โ CORRECT - Use HashSet to track seen
if (!seen.contains(newUgly)) {
heap.offer(newUgly);
seen.add(newUgly);
}
๐ฏ Pattern Recognition
Problem Type: Sequence Generation with Multiple Sources
Core Pattern: Three Pointers / Multiple Sequence Merge
When to Apply:
โ Generate sequence from multiple sources
โ Need nth element in merged sequence
โ Each source follows a pattern
โ Must merge in sorted order
Recognition Keywords:
- "nth element"
- "prime factors limited to..."
- "generate sequence"
- "merge multiple sequences"
Similar Problems:
- Super Ugly Number (LC 313) - k factors instead of 3
- Merge K Sorted Lists (LC 23) - merge k sequences
- Kth Smallest in Multiplication Table (LC 668)
Key Components:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Multiple Sequences: ร2, ร3, ร5 โ
โ Pointers: Track position in each โ
โ Pick Minimum: Merge in order โ
โ Advance: Pointer(s) that gave min โ
โ DP Array: Store results โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ Quick Revision Notes
๐ฏ Core Concept:
Generate nth ugly number (only factors 2,3,5). Brute force: Check each number O(nรlog m). Better: Min-heap generates only ugly O(n log n). Optimal: Three pointers DP O(n) - maintain sequences ร2, ร3, ร5, always pick minimum, advance pointer(s) that gave min. Handles duplicates automatically!
โก Quick Implementations:
import java.util.HashSet;
import java.util.PriorityQueue;
public class Solution {
public int nthUglyNumber(int n) {
// First 15 ugly numbers:
// 1 = 1
// 2 = 2
// 3 = 3
// 4 = 2 ร 2
// 5 = 5
// 6 = 2 ร 3
// 8 = 2 ร 2 ร 2
// 9 = 3 ร 3
// 10 = 2 ร 5
// 12 = 2 ร 2 ร 3
// 15 = 3 ร 5
// 16 = 2 ร 2 ร 2 ร 2
// 18 = 2 ร 3 ร 3
// 20 = 2 ร 2 ร 5
// 24 = 2 ร 2 ร 2 ร 3
// return naive(n);
// return minHeap(n);
return optimal(n);
}
private int optimal(int n) {
// step 1: base case
if (n == 1) {
return 1;
}
// step 2: maintain an ugly array
int[] ugly = new int[n];
ugly[0] = 1;
// step 3: we know that ugly numbers are generated by any one
// of earlier ugly numbers multiplied by 2 or 3 or 5
// step 4: lets maintain 3 pointers which are basically
// indices and are initially at 0. Multiply ugly[p2], ugly[p3]
// and ugly[p5] with 2, 3 and 5.
// For exmaple, ugly[1] = min(2,3,5).
int p2 = 0;
int p3 = 0;
int p5 = 0;
// step 5: calculate ugly array
for (int i = 1; i < n; i++) {
int ugly2 = ugly[p2] * 2;
int ugly3 = ugly[p3] * 3;
int ugly5 = ugly[p5] * 5;
// step 5: Whatever pointer contributed min out of (p2,p3,p5),
// increment that and repeat the same.
// step 6: Why that pointer increment?
// If we do not increment, we would be still getting 2 forever.
// Incrementing it would be providing 4 next time.
// Why other pointers not incremented?
// Still, 3 and 5 not added.
int newUgly = Math.min(ugly2, Math.min(ugly3, ugly5));
ugly[i] = newUgly;
// step 7: if both pointers are contributing minimu, increment both.
// This is another way to avoid duplicates
if (newUgly == ugly2) {
p2++;
}
if (newUgly == ugly3) {
p3++;
}
if (newUgly == ugly5) {
p5++;
}
}
return ugly[n - 1];
}
private int minHeap(int n) {
// step 1: base case
if (n == 1) {
return 1;
}
// step 2 - create a min heap with initial state and also visited set
PriorityQueue<Long> pq = new PriorityQueue<>();
pq.offer(1L);
HashSet<Long> visited = new HashSet<>();
// step 3: main concept is that an ugly number is always get derived
// from earlier ugly number multiplied by 2 or 3 or 5
// So, keep on generating the ugly numbers and push to queue till n.
// It will happen like [1,2,3,5,4,6,10,6,9,15,10,15,25]
// But we get correct result because we maintain visited set and min heap
for (int i = 2; i <= n; i++) {
long polled = pq.poll();
long newUgly = polled * 2;
if (!visited.contains(newUgly)) {
pq.offer(newUgly);
visited.add(newUgly);
}
newUgly = polled * 3;
if (!visited.contains(newUgly)) {
pq.offer(newUgly);
visited.add(newUgly);
}
newUgly = polled * 5;
if (!visited.contains(newUgly)) {
pq.offer(newUgly);
visited.add(newUgly);
}
}
long res = pq.poll();
return (int) res;
}
private int naive(int n) {
// step 1: base case
if (n == 1) {
return 1;
}
// step 2: 1 is always included as ugly number by default
// so start with 2 and count with 1 (as i is already counted)
int num = 2;
int count = 1;
while (count <= n) {
// step 3: check if existing number is ugly.
// Then only count it and if we got the nth num, return it.
if (isUgly(num)) {
count++;
}
if (count == n) {
break;
}
num++;
}
return num;
}
private boolean isUgly(int num) {
if (num <= 0) {
return false;
}
// prime factorization with 2, 3 and 5
while (num % 2 == 0) {
num = num / 2;
}
while (num % 3 == 0) {
num = num / 3;
}
while (num % 5 == 0) {
num = num / 5;
}
return num == 1;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.nthUglyNumber(1) == 1);
System.out.println(s.nthUglyNumber(2) == 2);
System.out.println(s.nthUglyNumber(3) == 3);
System.out.println(s.nthUglyNumber(10) == 12);
}
}
๐ Key Insights:
- Natural Progression: Check all โ Generate ugly โ Three pointers
- First Insight: Every ugly = previous ร {2,3,5}
- Second Insight: Track three sequences separately
- Three Pointers: p2, p3, p5 - all start at 0
- Pick Minimum:
Math.min(next2, Math.min(next3, next5)) - Advance All Matches: Handles duplicates (6 from both 2ร3 and 3ร2)
- Growth: O(nรlog m) โ O(n log n) โ O(n)! โ
๐ช Memory Aid:
"Three sequences: ร2, ร3, ร5!"
"Pick minimum, advance pointer(s)!"
"This is DP with pointers - brilliant!" โจ
๐งช Edge Cases
Case 1: n = 1
Input: n = 1
Output: 1
First ugly number is 1
Case 2: Small n
Input: n = 10
Output: 12
[1,2,3,4,5,6,8,9,10,12]
Case 3: Large n
Input: n = 1690
Output: 2123366400
DP handles efficiently!
All handled correctly! โ
๐ Complexity Analysis
Approach 1: Brute Force
Time: O(n ร log m)
Check n ugly numbers
Each check: O(log m) divisions
m = nth ugly number (can be huge!)
Space: O(1)
Approach 2: Min-Heap
Time: O(n log n)
n iterations
Each: poll O(log n) + 3 offers O(log n)
Space: O(n)
Heap + HashSet
Approach 3: Three Pointers DP
Time: O(n)
Single pass
Each iteration: O(1) operations
Space: O(n)
Array storage only
Optimal! Can't do better than O(n)!
Happy practicing! ๐ฏ
Note: This problem is a MASTERCLASS in optimization! You naturally start with brute force (check all), realize you can generate only ugly numbers (min-heap), then discover an even better way (three pointers DP). Each step builds on the previous insight. The three pointers pattern is BEAUTIFUL - it tracks three sequences, picks minimum, and handles duplicates automatically. This progression from O(nรlog m) โ O(n log n) โ O(n) teaches you how to THINK about optimizations, not just memorize solutions. The DP pattern here (multiple pointers merging sequences) appears in many problems! True learning! ๐ชโจ๐