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 are given a list of words sorted lexicographically in an unknown language. You need to determine the order of characters in this alien language. Which algorithmic approach guarantees an optimal solution to find a valid character order?
easy
A. Topological sort using Kahn's algorithm with cycle detection and edge deduplication
B. Greedy approach comparing adjacent words without cycle detection
C. Dynamic programming to find longest common subsequence between words
D. Brute force pairwise comparison without graph construction
Solution
Step 1: Understand problem requires ordering characters based on precedence constraints
Characters form a directed graph where edges represent order constraints derived from adjacent words.
Step 2: Identify algorithm that finds linear order respecting all edges and detects cycles
Kahn's algorithm performs topological sort with cycle detection and can handle duplicate edges efficiently.
Final Answer:
Option A -> Option A
Quick Check:
Topological sort with cycle detection is standard for ordering with partial constraints [OK]
Hint: Topological sort solves ordering with cycle detection [OK]
Common Mistakes:
Assuming greedy comparison suffices
Using DP unrelated to ordering
Ignoring cycle detection
2. Consider the following Python code snippet implementing the optimal backtracking solution for Expression Add Operators. Given input num = "123" and target = 6, what is the final list of expressions returned by the function?
easy
A. ["1+2+3", "1*2*3"]
B. ["1+2+3", "1*2*3", "12-6"]
C. ["1+2+3", "1*2*3", "1+23"]
D. ["1+2+3", "1*2*3", "1-2+3"]
Solution
Step 1: Trace backtrack calls for num="123", target=6
At index 0, path starts with "1". Then operators +, -, * are tried with "2" and "3". Valid expressions that evaluate to 6 are "1+2+3" and "1*2*3".
Step 2: Verify no other expressions equal 6
Expressions like "12-6" or "1+23" do not evaluate to 6. "1-2+3" equals 2, so not included.
Final Answer:
Option A -> Option A
Quick Check:
Only "1+2+3" and "1*2*3" sum to 6 [OK]
Hint: Trace small input carefully to confirm valid expressions [OK]
Common Mistakes:
Including invalid expressions with wrong evaluation
Missing multiplication precedence
3. Given the following LFUCache implementation, what is the return value of cache.get(2) after executing the sequence below?
After put(1,1) and put(2,2), cache has keys 1 and 2 with freq=1. get(1) increments freq of key 1 to 2. Then put(3,3) triggers eviction: key 2 has freq=1, key 1 has freq=2, so key 2 is evicted. Key 3 inserted with freq=1.
Step 2: Evaluate get(2)
Key 2 was evicted, so get(2) returns -1.
Final Answer:
Option D -> Option D
Quick Check:
Eviction removed key 2 -> get(2) returns -1 [OK]
Hint: Eviction removes least freq and LRU key -> get returns -1 [OK]
Common Mistakes:
Assuming key 2 still exists after eviction
Confusing frequency increments
Off-by-one in frequency updates
4. Given the following Python code snippet implementing the optimized trie approach, what is the output of maximizeXor(nums, queries) for nums = [0,1,2] and queries = [[3,1],[1,2]]?
class TrieNode:
def __init__(self):
self.children = [null, null]
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, num):
node = self.root
for i in range(31, -1, -1):
bit = (num >> i) & 1
if not node.children[bit]:
node.children[bit] = TrieNode()
node = node.children[bit]
def maxXor(self, num):
node = self.root
if not node.children[0] and not node.children[1]:
return -1
max_xor = 0
for i in range(31, -1, -1):
bit = (num >> i) & 1
toggled_bit = 1 - bit
if node.children[toggled_bit]:
max_xor |= (1 << i)
node = node.children[toggled_bit]
else:
node = node.children[bit]
return max_xor
def maximizeXor(nums, queries):
nums.sort()
queries = sorted([(x, m, i) for i, (x, m) in enumerate(queries)], key=lambda x: x[1])
res = [-1] * len(queries)
trie = Trie()
idx = 0
n = len(nums)
for x, m, i in queries:
while idx < n and nums[idx] <= m:
trie.insert(nums[idx])
idx += 1
res[i] = trie.maxXor(x)
return res
print(maximizeXor(nums, queries))
easy
A. [3, 3]
B. [2, 3]
C. [3, 1]
D. [-1, 3]
Solution
Step 1: Process first query (3,1)
Only nums ≤ 1 are [0,1]. Insert 0 and 1 into trie. maxXor(3) with {0,1} is max(3^0=3,3^1=2)=3.
Step 2: Process second query (1,2)
Now nums ≤ 2 are [0,1,2]. Insert 2. maxXor(1) with {0,1,2} is max(1^0=1,1^1=0,1^2=3)=3.
Final Answer:
Option B -> Option B
Quick Check:
Results match expected XOR values [OK]
Hint: Insert nums ≤ m incrementally, then maxXor query [OK]
Common Mistakes:
Forgetting to sort queries and nums
Not inserting all eligible nums before query
Returning -1 when trie is empty incorrectly
5. What is the time complexity of the bidirectional BFS with state compression algorithm for the shortest path visiting all nodes problem on a graph with n nodes?
medium
A. O(n * 2^{n/2})
B. O(n * 2^n)
C. O(n^2 * 2^{n/2})
D. O(2^n)
Solution
Step 1: Understand state space size
Bidirectional BFS splits the bitmask state space into two halves, each of size 2^{n/2}, reducing the exponential factor.
Step 2: Calculate complexity
Each half explores O(n * 2^{n/2}) states (n nodes times subsets of half the nodes). Combining both halves yields O(n * 2^{n/2}) overall time complexity.
Final Answer:
Option A -> Option A
Quick Check:
Bidirectional BFS halves the exponent in the state space [OK]
Hint: Bidirectional BFS halves exponent in state space complexity [OK]
Common Mistakes:
Assuming complexity is O(n * 2^n) like unidirectional BFS
Confusing node count with edge count in complexity
Overestimating complexity by including squared terms unnecessarily
Recommended
Ready to prove it?
Pass the test and this pattern moves unknownstrongwhile earning +1 Grip toward your certificate.