Start with the original list of envelopes: [[5,4],[6,4],[6,7],[2,3]]. No operations performed yet.
💡 Seeing the raw input helps understand what data the algorithm will process and why sorting is necessary.
Line:test = [[5,4],[6,4],[6,7],[2,3]]
💡 The envelopes are unordered and contain duplicates in width, which sorting will address.
setup
Sort Envelopes by Width Ascending and Height Descending
Sort envelopes by width ascending and for equal widths by height descending. The sorted order becomes [[2,3],[5,4],[6,7],[6,4]].
💡 Sorting this way ensures envelopes with the same width cannot be nested incorrectly by height, which is critical for the LIS step.
Line:envelopes.sort(key=lambda x: (x[0], -x[1]))
💡 Sorting by width ascending and height descending handles duplicates correctly and prepares the data for LIS on heights.
setup
Extract Heights Array
Extract the heights from the sorted envelopes into a separate array: [3,4,7,4]. This array will be used for LIS calculation.
💡 Separating heights simplifies the LIS problem to a one-dimensional array, making it easier to apply binary search.
Line:heights = [h for _, h in envelopes]
💡 The problem reduces to finding the longest increasing subsequence in the heights array.
setup
Initialize tails Array
Initialize an empty tails array which will store the smallest tail of all increasing subsequences found so far.
💡 The tails array helps efficiently track the LIS length and maintain minimal possible tails for subsequences.
Line:tails = []
💡 Starting with an empty tails array prepares for building the LIS incrementally.
fill_cells
Process height 3: Append to tails
Process the first height 3. Since tails is empty, append 3 to tails.
💡 Appending the first height starts the LIS with length 1.
Line:idx = bisect.bisect_left(tails, h)
if idx == len(tails):
tails.append(h)
💡 The tails array now contains [3], representing an increasing subsequence of length 1 ending with 3.
fill_cells
Process height 4: Append to tails
Process height 4. Binary search finds position 1 (end of tails), so append 4 to tails.
💡 Appending a larger height extends the LIS length to 2.
Line:idx = bisect.bisect_left(tails, h)
if idx == len(tails):
tails.append(h)
💡 Tails now [3,4] means LIS length is 2 with smallest tails 3 and 4 respectively.
fill_cells
Process height 7: Append to tails
Process height 7. Binary search finds position 2 (end of tails), so append 7 to tails.
💡 Appending 7 extends the LIS length to 3.
Line:idx = bisect.bisect_left(tails, h)
if idx == len(tails):
tails.append(h)
💡 Tails now [3,4,7] means LIS length is 3 with smallest tails 3,4,7.
fill_cells
Process height 4: Replace in tails
Process height 4 again. Binary search finds position 1 in tails. Replace tails[1] with 4 to maintain the smallest possible tail for subsequences of length 2.
💡 Replacing maintains the smallest possible tail for subsequences of length 2, keeping tails optimal.
Line:idx = bisect.bisect_left(tails, h)
else:
tails[idx] = h
After processing all heights, calculate the length of tails, which is 3.
💡 The length of tails directly gives the length of the longest increasing subsequence.
Line:answer = len(tails)
💡 The LIS length corresponds to the maximum number of envelopes that can be nested.
reconstruct
Return Final Answer
Return the length of tails, 3, as the maximum number of envelopes that can be nested.
💡 Returning the final answer completes the algorithm.
Line:return len(tails)
💡 The final answer solves the original Russian Doll Envelopes problem.
from typing import List
import bisect
def maxEnvelopes(envelopes: List[List[int]]) -> int:
# STEP 2: Sort envelopes by width ascending and height descending
envelopes.sort(key=lambda x: (x[0], -x[1]))
# STEP 3: Extract heights
heights = [h for _, h in envelopes]
# STEP 4: Initialize tails array
tails = []
for i, h in enumerate(heights):
# STEP 5-8: Binary search to find insert position
idx = bisect.bisect_left(tails, h)
if idx == len(tails):
# STEP 5-7: Append if h is greater than all tails
tails.append(h)
else:
# STEP 8: Replace tails[idx] with h
tails[idx] = h
# STEP 9-10: Return length of tails as answer
return len(tails)
if __name__ == '__main__':
test = [[5,4],[6,4],[6,7],[2,3]]
print(maxEnvelopes(test))
📊
Russian Doll Envelopes - Watch the Algorithm Execute, Step by Step
Watching each step of sorting, extracting heights, and building the LIS with binary search reveals how the algorithm efficiently solves the problem, which is hard to grasp from code alone.
Step 1/10
·Active fill★Answer cell
none
[5,4]
0
[6,4]
1
[6,7]
2
[2,3]
3
sort
[2,3]
0
[5,4]
1
[6,7]
2
[6,4]
3
extract
3
0
4
1
7
2
4
3
initialize
Result: []
insert
i
3
0
Result: [3]
insert
3
0
i
4
1
Result: [3,4]
insert
3
0
4
1
i
7
2
Result: [3,4,7]
replace
3
0
4
1
7
2
Result: [3,4,7]
calculate_length
3
0
4
1
7
2
Result: 3
result
3
0
4
1
7
2
Result: 3
Key Takeaways
✓ Sorting envelopes by width ascending and height descending is crucial to handle envelopes with equal widths correctly.
Without this sorting order, the LIS on heights would count invalid nestings, which is hard to detect by just reading code.
✓ The tails array maintains the smallest possible tail for increasing subsequences of each length, enabling efficient LIS calculation.
This insight explains why replacing elements in tails keeps the LIS optimal, which is not obvious from code alone.
✓ The length of tails after processing all heights directly gives the maximum number of envelopes that can be nested.
Understanding this final step connects the LIS problem back to the original envelope nesting problem.
Practice
(1/5)
1. You have n people and a list of caps each person can wear. You want to count the number of ways to assign unique caps so that each person gets exactly one cap and no two people share the same cap. Which algorithmic approach guarantees an optimal solution for this problem?
easy
A. Sorting caps and assigning them in ascending order to people
B. Greedy algorithm that assigns the most popular cap first to maximize assignments
C. Simple recursion without memoization, trying all possible assignments
D. Bitmask dynamic programming that uses masks to track assigned caps and iterates over caps
Solution
Step 1: Understand the problem constraints
The problem requires counting all unique assignments where each person gets a unique cap, which involves combinatorial enumeration with uniqueness constraints.
Step 2: Identify the suitable approach
Bitmask DP efficiently tracks which caps are assigned using bitmasks, allowing counting without double counting or missing cases. Greedy or sorting approaches fail to guarantee all unique assignments are counted correctly.
Final Answer:
Option D -> Option D
Quick Check:
Bitmask DP is the standard for unique assignment counting problems [OK]
Hint: Use bitmask DP to track assigned caps uniquely [OK]
Common Mistakes:
Thinking greedy or sorting can count all unique assignments correctly
2. What is the time complexity of the optimal greedy solution that sorts pairs by their end values and then iterates through them once to find the longest chain?
medium
A. O(n log n)
B. O(n²)
C. O(n)
D. O(n log n + n²)
Solution
Step 1: Identify sorting cost
Sorting pairs by end values takes O(n log n) time.
Step 2: Identify iteration cost
Iterating through sorted pairs once is O(n).
Final Answer:
Option A -> Option A
Quick Check:
Dominant cost is sorting -> O(n log n) [OK]
Hint: Sorting dominates, iteration is linear [OK]
Common Mistakes:
Assuming iteration is O(n²) due to nested loops
Confusing DP approach complexity with greedy
Ignoring sorting cost
3. What is the time complexity of the topological sort based DP solution for the longest increasing path in an m x n matrix?
medium
A. O(m * n * log(m * n))
B. O(m * n^2)
C. O(m * n * 4^{m*n})
D. O(m * n)
Solution
Step 1: Identify sorting cost
Sorting all m*n cells by their values costs O(m*n*log(m*n)).
Step 2: Analyze DP updates
Each cell checks up to 4 neighbors, so DP updates cost O(m*n) total.
Final Answer:
Option A -> Option A
Quick Check:
Sorting dominates, so complexity is O(m*n*log(m*n)) [OK]
Hint: Sorting cells dominates complexity [OK]
Common Mistakes:
Assuming DP is O(m*n) ignoring sorting
Confusing exponential brute force with DP
Ignoring log factor from sorting
4. The following code attempts to count the number of longest increasing subsequences using DP arrays. Identify the bug that causes incorrect counting of LIS.
medium
A. counts array is initialized with zeros instead of ones
B. lengths array should be initialized with zeros instead of ones
C. The condition nums[j] < nums[i] should be nums[j] <= nums[i]
D. The final sum should be over counts where lengths are less than max_len
Solution
Step 1: Check initialization of counts array
Counts represent number of LIS ending at each index; initially each element alone forms LIS of length 1, so counts[i] must start at 1, not 0.
Step 2: Consequence of zero initialization
With counts initialized to 0, counts[i] remains 0 unless updated, causing undercounting of LIS and incorrect final result.
Final Answer:
Option A -> Option A
Quick Check:
Initialize counts with 1 to count single-element subsequences [OK]
Hint: Counts must start at 1 to count single elements [OK]
Common Mistakes:
Initializing counts with zeros
Using <= instead of <
Summing counts incorrectly
5. The following code attempts to solve the Travelling Salesman Problem using bottom-up bitmask DP. Identify the bug that causes incorrect results on some inputs.
def tsp_bottom_up_buggy(dist):
n = len(dist)
ALL_VISITED = (1 << n) - 1
dp = [[float('inf')] * n for _ in range(1 << n)]
# BUG: dp[1][0] not initialized
for mask in range(1 << n):
for i in range(n):
if mask & (1 << i):
for j in range(n):
if mask & (1 << j) and i != j:
prev_mask = mask ^ (1 << i)
dp[mask][i] = min(dp[mask][i], dp[prev_mask][j] + dist[j][i])
ans = float('inf')
for i in range(1, n):
ans = min(ans, dp[ALL_VISITED][i] + dist[i][0])
return ans
medium
A. Missing base case initialization dp[1][0] = 0
B. Incorrect calculation of prev_mask = mask ^ (1 << i)
C. Line initializing dp array with float('inf')
D. Final loop over i in range(1, n) missing city 0
Solution
Step 1: Check base case initialization
The DP table must have dp[1][0] = 0 to represent starting at city 0 with only city 0 visited.
Step 2: Consequence of missing base case
Without this, all dp states remain infinite, so no valid path cost is computed, causing incorrect results.
Final Answer:
Option A -> Option A
Quick Check:
Base case initialization is essential for DP correctness [OK]
Hint: Always initialize base case dp for starting city [OK]
Common Mistakes:
Forgetting dp[1][0] initialization leads to infinite costs
Recommended
Ready to prove it?
Pass the test and this pattern moves unknownstrongwhile earning +1 Grip toward your certificate.