A clear explanation of the Ugly Number II problem using dynamic programming with three pointers.
Problem Restatement
We are given an integer n.
Return the nth ugly number.
An ugly number is a positive integer whose prime factors are limited to:
2, 3, 5The sequence starts with:
1, 2, 3, 4, 5, 6, 8, 9, 10, 12, ...1 is treated as an ugly number.
The constraint is:
1 <= n <= 1690The official examples include n = 10 -> 12 and n = 1 -> 1.
Input and Output
| Item | Meaning |
|---|---|
| Input | Integer n |
| Output | The nth ugly number |
| Ugly factor rule | Prime factors may only be 2, 3, and 5 |
| First ugly number | 1 |
Example function shape:
def nthUglyNumber(n: int) -> int:
...Examples
Example 1:
n = 10The first ten ugly numbers are:
1, 2, 3, 4, 5, 6, 8, 9, 10, 12The 10th ugly number is:
12Answer:
12Example 2:
n = 1The first ugly number is:
1Answer:
1First Thought: Check Every Integer
A direct approach is to test every positive integer.
For each number, use the method from LeetCode 263:
- Divide out all factors of
2. - Divide out all factors of
3. - Divide out all factors of
5. - If the result is
1, the number is ugly.
Keep counting ugly numbers until the nth one appears.
This works, but it wastes time checking many numbers that are not ugly.
Problem With Brute Force
The ugly numbers become more spread out as values grow.
Checking every integer means we spend work on numbers like:
7, 11, 13, 14, 17, 19, 21, ...Most of these contain forbidden prime factors.
Instead of checking all numbers, we should generate ugly numbers directly.
Key Insight
Every ugly number after 1 comes from a smaller ugly number multiplied by one of:
2, 3, 5So if we already know the ugly sequence, the next ugly number must be the smallest value among:
some_previous_ugly * 2
some_previous_ugly * 3
some_previous_ugly * 5We maintain three pointers:
| Pointer | Meaning |
|---|---|
i2 | The smallest ugly number index whose * 2 candidate has not been used |
i3 | The smallest ugly number index whose * 3 candidate has not been used |
i5 | The smallest ugly number index whose * 5 candidate has not been used |
At each step, the next ugly number is:
min(ugly[i2] * 2, ugly[i3] * 3, ugly[i5] * 5)After choosing the next number, advance every pointer that produced it.
Advancing every matching pointer is important because duplicates can be generated in multiple ways.
For example:
6 = 2 * 3 = 3 * 2We should include 6 only once.
Algorithm
- Create an array
uglyof lengthn. - Set:
ugly[0] = 1 - Start three pointers:
i2 = i3 = i5 = 0 - For each next position from
1ton - 1:- Compute:
next2 = ugly[i2] * 2 next3 = ugly[i3] * 3 next5 = ugly[i5] * 5 - Let:
nxt = min(next2, next3, next5) - Store
nxtin the sequence. - If
nxt == next2, incrementi2. - If
nxt == next3, incrementi3. - If
nxt == next5, incrementi5.
- Compute:
- Return:
ugly[n - 1]
Walkthrough
Use:
n = 10Start:
ugly = [1]
i2 = 0
i3 = 0
i5 = 0Candidates:
| Step | ugly[i2] * 2 | ugly[i3] * 3 | ugly[i5] * 5 | Next |
|---|---|---|---|---|
| 1 | 2 | 3 | 5 | 2 |
| 2 | 4 | 3 | 5 | 3 |
| 3 | 4 | 6 | 5 | 4 |
| 4 | 6 | 6 | 5 | 5 |
| 5 | 6 | 6 | 10 | 6 |
| 6 | 8 | 9 | 10 | 8 |
| 7 | 10 | 9 | 10 | 9 |
| 8 | 10 | 12 | 10 | 10 |
| 9 | 12 | 12 | 15 | 12 |
So the first ten ugly numbers are:
[1, 2, 3, 4, 5, 6, 8, 9, 10, 12]Return:
12Correctness
The array ugly is built in increasing order.
Initially, ugly[0] = 1, which is the first ugly number.
Every later ugly number must be formed by multiplying a smaller ugly number by 2, 3, or 5. Therefore, the next possible ugly number must be one of the three candidate streams:
ugly[i2] * 2
ugly[i3] * 3
ugly[i5] * 5The pointers always refer to the first candidate in each stream that has not already been passed.
Taking the minimum of these three candidates gives the smallest ugly number not yet added.
When the chosen value equals one or more candidate streams, all matching pointers are advanced. This removes duplicates such as 6, 10, 15, and 30, which can be produced by more than one factor path.
Thus each step appends exactly the next ugly number, in order, without duplicates.
After filling n values, ugly[n - 1] is the nth ugly number.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(n) | We generate one ugly number per iteration |
| Space | O(n) | We store the first n ugly numbers |
Implementation
class Solution:
def nthUglyNumber(self, n: int) -> int:
ugly = [1] * n
i2 = 0
i3 = 0
i5 = 0
for i in range(1, n):
next2 = ugly[i2] * 2
next3 = ugly[i3] * 3
next5 = ugly[i5] * 5
nxt = min(next2, next3, next5)
ugly[i] = nxt
if nxt == next2:
i2 += 1
if nxt == next3:
i3 += 1
if nxt == next5:
i5 += 1
return ugly[n - 1]Code Explanation
The array starts with 1:
ugly = [1] * nThe first value is already correct, so we fill from index 1.
The three pointers start at the first ugly number:
i2 = 0
i3 = 0
i5 = 0At every step, compute the next possible value from each multiplication stream:
next2 = ugly[i2] * 2
next3 = ugly[i3] * 3
next5 = ugly[i5] * 5The smallest candidate becomes the next ugly number:
nxt = min(next2, next3, next5)
ugly[i] = nxtThen advance every pointer that produced nxt:
if nxt == next2:
i2 += 1
if nxt == next3:
i3 += 1
if nxt == next5:
i5 += 1Using three separate if statements removes duplicates.
Finally:
return ugly[n - 1]returns the nth ugly number.
Testing
def run_tests():
s = Solution()
assert s.nthUglyNumber(1) == 1
assert s.nthUglyNumber(2) == 2
assert s.nthUglyNumber(3) == 3
assert s.nthUglyNumber(10) == 12
assert s.nthUglyNumber(11) == 15
assert s.nthUglyNumber(15) == 24
assert s.nthUglyNumber(1690) == 2123366400
print("all tests passed")
run_tests()Test meaning:
| Test | Why |
|---|---|
1 | First ugly number |
2, 3 | Early sequence order |
10 | Official example |
11 | Checks next value after official prefix |
15 | Confirms duplicate handling across streams |
1690 | Upper constraint boundary |