10 · Project — Timed Practice Set 2¶
This set mixes every Level 2 pattern, and deliberately does not tell you which pattern each problem uses. Recognizing the pattern from the problem statement is half of what an interview tests.
Rules¶
- 120 minutes for six problems (about 20 each). If stuck at 25 minutes, write your best partial idea as comments and move on.
- Before coding each one, write a short plan: the pattern, the state or invariant, and the target time and space complexity.
- Test each solution with at least three asserts, including one edge case.
- Speak your reasoning out loud (or record yourself). You will review it afterwards.
Self-review rubric¶
Score each problem 0–2 on each row (max 10 per problem, 60 total):
| Criterion | 0 | 1 | 2 |
|---|---|---|---|
| Pattern | Did not identify | Identified after coding started | Identified during planning |
| Correctness | Fails tests | Passes with bugs fixed late | Passes first run |
| Complexity | Not stated / wrong | Stated, minor errors | Correct time and space |
| Edge cases | None considered | Some | Empty/single/duplicate/extreme all considered |
| Communication | Silent coding | Explained after the fact | Explained plan before coding |
The problems¶
- Level Averages. Given a binary tree, return the average value of the nodes on each level.
- Course Order Possible? There are
ncourses and prerequisite pairs[a, b]("take b before a"). Can all courses be finished? - Last Stone Weight. Repeatedly smash the two heaviest stones; if they differ, the difference remains. Return the last stone's weight, or 0.
- Combination Sum III. Find all combinations of
kdistinct numbers from 1–9 that sum ton. - Unique Paths With Obstacles. Count paths from top-left to bottom-right of a grid moving only right or down, avoiding cells marked 1.
- Employee Free Time (simplified). Given each employee's busy intervals (each list sorted), return the finite intervals when everyone is free.
Stop here until the timer ends.
Walkthrough 1 — level averages¶
Pattern: BFS by level (lesson 1). Snapshot the queue length to process one level at a time.
from collections import deque
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val, self.left, self.right = val, left, right
def level_averages(root):
if not root:
return []
out, queue = [], deque([root])
while queue:
size, total = len(queue), 0
for _ in range(size):
node = queue.popleft()
total += node.val
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
out.append(total / size)
return out
root = TreeNode(3, TreeNode(9), TreeNode(20, TreeNode(15), TreeNode(7)))
assert level_averages(root) == [3.0, 14.5, 11.0]
assert level_averages(None) == []
assert level_averages(TreeNode(-5)) == [-5.0]
O(n) time, O(w) space for the widest level.
Walkthrough 2 — can all courses be finished?¶
Pattern: cycle detection in a directed graph (lesson 4, previewing Level 3's topological sort). Courses can all be taken exactly when the prerequisite graph has no cycle. Use three colours: 0 = unvisited, 1 = on the current DFS path, 2 = finished. Meeting a node coloured 1 means a back edge — a cycle.
def can_finish(n, prerequisites):
graph = [[] for _ in range(n)]
for a, b in prerequisites:
graph[b].append(a)
state = [0] * n
def has_cycle(u):
state[u] = 1
for v in graph[u]:
if state[v] == 1:
return True
if state[v] == 0 and has_cycle(v):
return True
state[u] = 2
return False
return not any(state[u] == 0 and has_cycle(u) for u in range(n))
assert can_finish(2, [[1, 0]])
assert not can_finish(2, [[1, 0], [0, 1]])
assert can_finish(3, []) # edge: no prerequisites
assert not can_finish(1, [[0, 0]]) # edge: self-loop
assert can_finish(4, [[1, 0], [2, 0], [3, 1], [3, 2]]) # diamond, no cycle
A plain visited set is not enough: in the diamond case, node 3 is reached twice without any cycle. The "on current path" state is what distinguishes a cycle from a re-merge. O(V + E). (For very deep graphs, switch to Kahn's iterative algorithm from Level 3.)
Walkthrough 3 — last stone weight¶
Pattern: max-heap (lesson 3), simulated with negated values.
import heapq
def last_stone_weight(stones):
heap = [-s for s in stones]
heapq.heapify(heap)
while len(heap) > 1:
a = -heapq.heappop(heap)
b = -heapq.heappop(heap)
if a != b:
heapq.heappush(heap, -(a - b))
return -heap[0] if heap else 0
assert last_stone_weight([2, 7, 4, 1, 8, 1]) == 1
assert last_stone_weight([1]) == 1
assert last_stone_weight([3, 3]) == 0 # edge: all destroyed
assert last_stone_weight([]) == 0
O(n log n). Sorting the list after every smash would be O(n² log n).
Walkthrough 4 — combination sum III¶
Pattern: backtracking with increasing choices (lesson 5). Prune when the running sum
exceeds n or the combination is already k long.
def combination_sum3(k, n):
result, path = [], []
def backtrack(start, remaining):
if len(path) == k:
if remaining == 0:
result.append(path.copy())
return
for x in range(start, 10):
if x > remaining:
break
path.append(x)
backtrack(x + 1, remaining - x)
path.pop()
backtrack(1, n)
return result
assert combination_sum3(3, 7) == [[1, 2, 4]]
assert combination_sum3(3, 9) == [[1, 2, 6], [1, 3, 5], [2, 3, 4]]
assert combination_sum3(4, 1) == [] # edge: impossible
assert combination_sum3(9, 45) == [[1, 2, 3, 4, 5, 6, 7, 8, 9]]
The search space is at most C(9, k) ≤ 126 combinations, so this is effectively constant time — worth saying, since the interviewer may ask about complexity.
Walkthrough 5 — unique paths with obstacles¶
Pattern: 2-D DP (lesson 6). dp[c] = number of ways to reach the current row's cell
c. A cell's count is the sum of ways from above (the old dp[c]) and from the left
(dp[c-1]), or 0 if blocked. One row of storage suffices.
def unique_paths_with_obstacles(grid):
cols = len(grid[0])
dp = [0] * cols
dp[0] = 1
for row in grid:
for c in range(cols):
if row[c] == 1:
dp[c] = 0
elif c > 0:
dp[c] += dp[c - 1]
return dp[-1]
assert unique_paths_with_obstacles([[0, 0, 0], [0, 1, 0], [0, 0, 0]]) == 2
assert unique_paths_with_obstacles([[0, 1], [0, 0]]) == 1
assert unique_paths_with_obstacles([[1]]) == 0 # edge: start blocked
assert unique_paths_with_obstacles([[0, 0], [0, 1]]) == 0 # edge: end blocked
assert unique_paths_with_obstacles([[0] * 3 for _ in range(3)]) == 6
O(R · C) time, O(C) space. The start-blocked case works because the first row's
dp[0] = 0 propagates.
Walkthrough 6 — employee free time¶
Pattern: intervals (lesson 7). Flatten all busy intervals, merge them, and report the gaps between consecutive merged blocks.
def employee_free_time(schedules):
busy = sorted(iv for person in schedules for iv in person)
free, end = [], None
for s, e in busy:
if end is not None and s > end:
free.append([end, s])
end = e if end is None else max(end, e)
return free
assert employee_free_time([[[1, 2], [5, 6]], [[1, 3]], [[4, 10]]]) == [[3, 4]]
assert employee_free_time([[[1, 3], [6, 7]], [[2, 4]], [[2, 5], [9, 12]]]) == \
[[5, 6], [7, 9]]
assert employee_free_time([[[1, 5]]]) == [] # edge: one block, no gaps
assert employee_free_time([[[1, 2]], [[2, 3]]]) == [] # touching: no free time
O(N log N) for N total intervals. Because each person's list is already sorted, a k-way heap merge (lesson 3) gives O(N log k) — a good follow-up to offer.
After the set¶
Add up your rubric scores. Below 40 of 60: repeat the weakest two lessons and re-attempt those problems in a week. 40–50: move on, but add the missed patterns to a spaced review list. Above 50: you are ready for Level 3.
Look especially at the Pattern row. If you only recognized the pattern after coding had started, practice "pattern-only drills": read ten problem statements and, for each, write only the pattern and the key state or invariant in under two minutes, without coding.
How It Actually Works¶
This set interleaves six patterns on purpose. When you practise one topic at a time, you already know which tool to use before you read the problem, so the hardest interview skill — choosing the approach — never gets exercised. Mixed sets make practice feel harder and slower, but that difficulty is exactly the selection step you need to train.
Each walkthrough also shows how a Level 2 pattern reduces to a small invariant: BFS by
levels depends on the queue holding exactly one level at the start of each loop; the
three-colour DFS depends on "on the current path" meaning "an ancestor in the recursion";
the one-row DP depends on dp[c] still holding the previous row's value until it is
overwritten. When a solution fails, checking which invariant broke is usually faster
than re-reading the code line by line. The rubric turns a vague "that went okay" into
per-dimension scores you can compare from week to week.
Exercise¶
Pick the problem where you scored lowest. Write a variation of it that breaks your solution (for example, unique paths where you may also move diagonally, or course scheduling where you must output an order). Solve the variation, test it with at least four asserts, and write two sentences on what changed in the state, invariant, or complexity.