Initialize the min-heap with the first ugly number 1 and mark it as seen to avoid duplicates.
💡 This sets up the data structures that track candidates and prevent repeated numbers.
Line:heap = [1]
seen = {1}
💡 The heap starts with the smallest ugly number, and the seen set prevents duplicate insertions.
delete
Pop smallest number (1) from heap
Extract the smallest number 1 from the heap to generate new candidates by multiplying with 2, 3, and 5.
💡 Extracting the smallest number is the core operation to progress the sequence.
Line:ugly = heapq.heappop(heap)
💡 The smallest ugly number is now processed to generate further ugly numbers.
insert
Insert 2 (1*2) into heap
Multiply extracted number 1 by factor 2 to get 2, which is not seen, so insert it into the heap and mark as seen.
💡 Inserting new candidates expands the sequence of ugly numbers.
Line:nxt = ugly * 2
if nxt not in seen:
seen.add(nxt)
heapq.heappush(heap, nxt)
💡 New multiples are generated and added only if unique.
insert
Insert 3 (1*3) into heap
Multiply extracted number 1 by factor 3 to get 3, which is new, so insert it into the heap and mark as seen.
💡 Continuing to add new unique multiples to the heap.
Line:nxt = ugly * 3
if nxt not in seen:
seen.add(nxt)
heapq.heappush(heap, nxt)
💡 Heap now contains multiple candidates from different factors.
insert
Insert 5 (1*5) into heap
Multiply extracted number 1 by factor 5 to get 5, which is new, so insert it into the heap and mark as seen.
💡 Adding all factor multiples of the extracted number to the heap.
Line:nxt = ugly * 5
if nxt not in seen:
seen.add(nxt)
heapq.heappush(heap, nxt)
💡 Heap now contains the first set of candidates: 2, 3, and 5.
delete
Pop smallest number (2) from heap
Extract the smallest number 2 from the heap to generate new candidates by multiplying with 2, 3, and 5.
💡 Each extraction advances the sequence and triggers new insertions.
Line:ugly = heapq.heappop(heap)
💡 The next ugly number is 2, confirming the sequence order.
insert
Insert 4 (2*2) into heap
Multiply extracted number 2 by factor 2 to get 4, which is new, so insert it into the heap and mark as seen.
💡 Generating new candidates from the latest extracted ugly number.
Line:nxt = ugly * 2
if nxt not in seen:
seen.add(nxt)
heapq.heappush(heap, nxt)
💡 Heap grows with new multiples maintaining order.
insert
Insert 6 (2*3) into heap
Multiply extracted number 2 by factor 3 to get 6, which is new, so insert it into the heap and mark as seen.
💡 Continuing to add new unique multiples.
Line:nxt = ugly * 3
if nxt not in seen:
seen.add(nxt)
heapq.heappush(heap, nxt)
💡 Heap now contains 3, 5, 4, and 6.
insert
Insert 10 (2*5) into heap
Multiply extracted number 2 by factor 5 to get 10, which is new, so insert it into the heap and mark as seen.
💡 Adding all factor multiples of the extracted number to the heap.
Line:nxt = ugly * 5
if nxt not in seen:
seen.add(nxt)
heapq.heappush(heap, nxt)
💡 Heap now contains 3, 5, 4, 6, and 10.
delete
Pop smallest number (3) from heap
Extract the smallest number 3 from the heap to generate new candidates by multiplying with 2, 3, and 5.
💡 Each extraction advances the sequence and triggers new insertions.
Line:ugly = heapq.heappop(heap)
💡 The next ugly number is 3, confirming the sequence order.
insert
Insert 6 (3*2) into heap
Multiply extracted number 3 by factor 2 to get 6, which is already seen, so skip insertion.
💡 Duplicates are skipped to keep heap size manageable.
Line:nxt = ugly * 2
if nxt not in seen:
seen.add(nxt)
heapq.heappush(heap, nxt)
💡 Seen set prevents duplicate insertions.
insert
Insert 9 (3*3) into heap
Multiply extracted number 3 by factor 3 to get 9, which is new, so insert it into the heap and mark as seen.
💡 New unique multiples are added to the heap.
Line:nxt = ugly * 3
if nxt not in seen:
seen.add(nxt)
heapq.heappush(heap, nxt)
💡 Heap grows with new candidates maintaining order.
insert
Insert 15 (3*5) into heap
Multiply extracted number 3 by factor 5 to get 15, which is new, so insert it into the heap and mark as seen.
💡 Adding all factor multiples of the extracted number to the heap.
Line:nxt = ugly * 5
if nxt not in seen:
seen.add(nxt)
heapq.heappush(heap, nxt)
💡 Heap now contains 4, 5, 10, 6, 9, and 15.
delete
Pop smallest number (4) from heap
Extract the smallest number 4 from the heap to generate new candidates by multiplying with 2, 3, and 5.
💡 Each extraction advances the sequence and triggers new insertions.
Line:ugly = heapq.heappop(heap)
💡 The next ugly number is 4, confirming the sequence order.
insert
Insert 8 (4*2) into heap
Multiply extracted number 4 by factor 2 to get 8, which is new, so insert it into the heap and mark as seen.
💡 Generating new candidates from the latest extracted ugly number.
Line:nxt = ugly * 2
if nxt not in seen:
seen.add(nxt)
heapq.heappush(heap, nxt)
💡 Heap grows with new multiples maintaining order.
insert
Insert 12 (4*3) into heap
Multiply extracted number 4 by factor 3 to get 12, which is new, so insert it into the heap and mark as seen.
💡 Continuing to add new unique multiples.
Line:nxt = ugly * 3
if nxt not in seen:
seen.add(nxt)
heapq.heappush(heap, nxt)
💡 Heap now contains 5, 6, 10, 15, 9, 8, and 12.
insert
Insert 20 (4*5) into heap
Multiply extracted number 4 by factor 5 to get 20, which is new, so insert it into the heap and mark as seen.
💡 Adding all factor multiples of the extracted number to the heap.
Line:nxt = ugly * 5
if nxt not in seen:
seen.add(nxt)
heapq.heappush(heap, nxt)
💡 Heap now contains 5, 6, 10, 15, 9, 8, 12, and 20.
delete
Pop smallest number (5) from heap
Extract the smallest number 5 from the heap to generate new candidates by multiplying with 2, 3, and 5.
💡 Each extraction advances the sequence and triggers new insertions.
Line:ugly = heapq.heappop(heap)
💡 The next ugly number is 5, confirming the sequence order.
insert
Insert 10 (5*2) into heap
Multiply extracted number 5 by factor 2 to get 10, which is already seen, so skip insertion.
💡 Duplicates are skipped to keep heap size manageable.
Line:nxt = ugly * 2
if nxt not in seen:
seen.add(nxt)
heapq.heappush(heap, nxt)
💡 Seen set prevents duplicate insertions.
insert
Insert 15 (5*3) into heap
Multiply extracted number 5 by factor 3 to get 15, which is already seen, so skip insertion.
💡 Duplicates are skipped to keep heap size manageable.
Line:nxt = ugly * 3
if nxt not in seen:
seen.add(nxt)
heapq.heappush(heap, nxt)
💡 Seen set prevents duplicate insertions.
import heapq
def nthUglyNumber(n):
heap = [1] # STEP 1
seen = {1} # STEP 1
ugly = 1
for _ in range(n): # STEP 2-20
ugly = heapq.heappop(heap) # STEP 2,6,10,14,18
for f in [2, 3, 5]: # STEP 3-5,7-9,11-13,15-17,19-20
nxt = ugly * f
if nxt not in seen:
seen.add(nxt)
heapq.heappush(heap, nxt)
return ugly # STEP 18
if __name__ == '__main__':
print(nthUglyNumber(10)) # Expected output: 12
📊
Ugly Number II (Merge 3 Sequences) - Watch the Algorithm Execute, Step by Step
Watching this step-by-step heap operation reveals how the algorithm efficiently merges three sequences without duplicates, which is hard to grasp from code alone.
Step 1/20
·Active fill★Answer cell
insertmin-heap
1
0
extract_minmin-heap
empty heap
Added: 1
insertmin-heap
2
0
insertmin-heap
2
0
3
1
insertmin-heap
2
0
3
1
5
2
extract_minmin-heapswap [0↔1]
3
0
5
1
Added: 2
insertmin-heapswap [2↔0]
3
0
5
1
4
2
insertmin-heap
3
0
5
1
4
2
6
3
insertmin-heap
3
0
5
1
4
2
6
3
10
4
extract_minmin-heapswap [0↔1]
4
0
5
1
10
2
6
3
Added: 3
peekmin-heap
4
0
5
1
10
2
6
3
insertmin-heap
4
0
5
1
10
2
6
3
9
4
insertmin-heap
4
0
5
1
10
2
6
3
9
4
15
5
extract_minmin-heapswap [0↔1]
5
0
6
1
10
2
15
3
9
4
Added: 4
insertmin-heap
5
0
6
1
10
2
15
3
9
4
8
5
insertmin-heap
5
0
6
1
10
2
15
3
9
4
8
5
12
6
insertmin-heap
5
0
6
1
10
2
15
3
9
4
8
5
12
6
20
7
extract_minmin-heapswap [0↔1]
6
0
9
1
10
2
15
3
20
4
8
5
12
6
Added: 5
peekmin-heap
6
0
9
1
10
2
15
3
20
4
8
5
12
6
peekmin-heap
6
0
9
1
10
2
15
3
20
4
8
5
12
6
Key Takeaways
✓ The min-heap always contains the next smallest candidates from the merged sequences of multiples of 2, 3, and 5.
This insight is hard to see from code alone because the heap operations and duplicate checks are implicit.
✓ Duplicates are prevented by the 'seen' set, ensuring the heap never contains repeated numbers, which keeps the algorithm efficient.
Understanding how duplicates are avoided visually clarifies why the heap size remains manageable.
✓ Each extraction from the heap corresponds to the next ugly number in ascending order, showing the algorithm's correctness.
Watching the heap pop the smallest element step-by-step confirms the sequence order.
Practice
(1/5)
1. Consider the following Python code that merges k sorted arrays using divide and conquer. Given the input arrays [[1,4,5],[1,3,4],[2,6]], what is the value of merged_arrays after the first iteration of the while loop inside merge_k_sorted_arrays?
easy
A. [[1,1,3,4,4,5,2,6]]
B. [[1,1,3,4,4,5],[2,6]]
C. [[1,3,4,5],[1,2,4,6]]
D. [[1,1,3,4,4,5],[2,6]]
Solution
Step 1: Trace merging pairs in first iteration
Arrays are [[1,4,5],[1,3,4],[2,6]]. Merge first two: merge_two_arrays([1,4,5],[1,3,4]) -> [1,1,3,4,4,5]. The last array [2,6] remains as is because no pair.
Step 2: Form merged_arrays after first iteration
merged_arrays = [[1,1,3,4,4,5],[2,6]]
Final Answer:
Option D -> Option D
Quick Check:
First pair merged correctly, last array unchanged -> matches [[1,1,3,4,4,5],[2,6]] [OK]
Hint: Merge pairs left to right, last array unchanged if odd count [OK]
Common Mistakes:
Merging last array incorrectly
Off-by-one in loop
Appending arrays instead of merging
2. Given the following code snippet for merging k sorted lists using divide and conquer, what is the value of lists[0] after the first iteration of the outer while loop when input lists are: List 0: 1 -> 4 -> 5 List 1: 1 -> 3 -> 4 List 2: 2 -> 6
easy
A. Merged list starting with 1 -> 1 -> 3 -> 4 -> 4 -> 5
B. Merged list starting with 1 -> 2 -> 3 -> 4 -> 5 -> 6
C. Merged list starting with 1 -> 1 -> 2 -> 3 -> 4 -> 4 -> 5 -> 6
D. Merged list starting with 1 -> 3 -> 4 -> 5 -> 6
Solution
Step 1: Trace first iteration of outer while loop
Interval = 1, merge lists[0] and lists[1]: merge (1->4->5) and (1->3->4).
Step 2: Result of mergeTwoLists(lists[0], lists[1])
Merged list is 1 -> 1 -> 3 -> 4 -> 4 -> 5. lists[2] remains unchanged.
Final Answer:
Option A -> Option A
Quick Check:
Only first two lists merged in first iteration [OK]
Hint: First merge merges pairs at interval 1 only [OK]
Common Mistakes:
Merging all lists at once instead of pairs
Confusing interval doubling
Assuming full merge after first iteration
3. You have an array where each element is at most k positions away from its sorted position. Which algorithmic approach guarantees the most efficient sorting in terms of time complexity?
easy
A. Use a standard full sort like quicksort or mergesort on the entire array.
B. Use a min-heap of size k+1 to iteratively extract the smallest element and build the sorted array.
C. Apply a greedy approach by swapping elements only when they are out of order by more than k positions.
D. Use dynamic programming to find the minimum number of swaps needed to sort the array.
Solution
Step 1: Understand the problem constraints
The array is almost sorted, with each element at most k positions away from its correct position.
Step 2: Identify the optimal approach
A min-heap of size k+1 efficiently keeps track of the next smallest element within the window of possible positions, allowing sorting in O(n log k) time.
Final Answer:
Option B -> Option B
Quick Check:
Min-heap size k+1 matches the maximum displacement constraint [OK]
4. Given the following code to find the 7th super ugly number with primes [2,3,5], what is the returned value?
easy
A. 8
B. 9
C. 10
D. 12
Solution
Step 1: Trace initial heap and ugly list
Start with ugly=[1], heap=[(2,0,0),(3,1,0),(5,2,0)]. Pop smallest val=2, append 2 -> ugly=[1,2]. Push (2*ugly[1])=4.
Step 2: Continue popping and pushing
Sequence generated: 1,2,3,4,5,6,8,10,... The 7th super ugly number is 10.
Final Answer:
Option C -> Option C
Quick Check:
Manual heap trace confirms 7th number is 10 [OK]
Hint: Trace heap pops and pushes carefully to avoid off-by-one errors [OK]
Common Mistakes:
Off-by-one in counting ugly numbers
Confusing index for ugly array
Skipping duplicates incorrectly
5. Suppose you want to merge k sorted arrays but now each array can contain duplicate elements that can be reused multiple times in the final merged array. Which modification to the divide and conquer approach correctly handles this scenario?
hard
A. Use a min heap to repeatedly extract the smallest element and reinsert it if duplicates can be reused.
B. Modify merge_two_arrays to allow unlimited reuse by appending elements multiple times without advancing indices.
C. No change needed; the existing divide and conquer merge works correctly with duplicates and reuse.
D. Switch to a dynamic programming approach to track counts of duplicates and reuse them optimally.
Solution
Step 1: Understand reuse of duplicates
Reusing duplicates means elements can appear multiple times beyond their original count, which standard merging does not support.
Step 2: Evaluate approaches
Divide and conquer merges fixed arrays once, so it cannot handle reuse. Modifying merge_two_arrays to append without advancing indices causes infinite loops. Dynamic programming is not suitable here. Using a min heap allows extracting smallest elements and reinserting them if reuse is allowed, correctly handling duplicates and reuse.
Final Answer:
Option A -> Option A
Quick Check:
Min heap supports repeated extraction and reinsertion -> handles reuse [OK]
Hint: Reuse requires dynamic extraction and reinsertion, not fixed merges [OK]
Common Mistakes:
Assuming divide and conquer handles reuse
Infinite loops by not advancing indices
Using DP unnecessarily
Recommended
Ready to prove it?
Pass the test and this pattern moves unknownstrongwhile earning +1 Grip toward your certificate.