Drill — Solutions Hidden by Default

Coding Problems Worked Out

Three commonly-named problems for AI engineering loops — Longest Common Prefix, Valid Anagram, Subarray Sum Equals K — plus eight common companions. Click to reveal solutions only after you've tried.

🎯 11 problems 🐍 Python ⏱ ~25 min each on first pass 🔁 Practice tracker saves locally
0 / 11 practiced

How to approach each problem out loud

Before solving anything, walk through this template. Practiced consistently, it makes you look senior even on problems you don't know.

  1. Restate: "Just to make sure I understand — you want me to..."
  2. Constraints: input size, sorted?, duplicates?, empty input?
  3. Example: trace a small case on paper or shared doc
  4. Brute force: mention it even if you'll discard it
  5. Pattern: consult the menu — hash map? two pointers? sliding window? DFS?
  6. State complexity: "brute force is O(n²); I think we can do O(n) with..."
  7. Code with narration: talk while you type
  8. Test: walk your code against the example
  9. Tradeoffs: time, space, edge cases, what you'd add for production

Pattern recognition menu

When you see a problem, scan this:

Problem says...Likely pattern
"Find pair / triplet summing to..."Hash map (or two pointers if sorted)
"Subarray / substring with property..."Sliding window / prefix sum
"Sorted array..."Two pointers, binary search
"Frequency / count / duplicates..."Hash map
"Tree / graph traversal..."DFS or BFS
"Shortest path / level by level..."BFS
"All paths / combinations..."DFS / backtracking
"Top K / Kth largest..."Heap or quickselect
"Anagram / palindrome..."Char counts (hash) or two pointers
"Overlapping intervals..."Sort by start, sweep

1Longest Common Prefix

StringMulti-pointer · LeetCode 14 · Easy

Prompt: Given an array of strings, return the longest common prefix. If none, return "".

examples
["flower", "flow", "flight"]   → "fl"
["dog", "racecar", "car"]      → ""
["interspecies", "interstellar", "interstate"] → "inters"

Edge cases to mention: empty input, one string, two strings where one is a prefix of another (["ab", "abc"] → "ab"), all empty strings.

Approach 1 — Vertical scan (cleanest, O(S))

Walk down the first string character by character, checking that every other string has the same character at that position.

vertical_scan.py
def longest_common_prefix(strs):
    if not strs:
        return ""
    for i in range(len(strs[0])):
        c = strs[0][i]
        for s in strs[1:]:
            if i >= len(s) or s[i] != c:
                return strs[0][:i]
    return strs[0]

Time: O(S) where S = sum of all characters across all strings — worst case you compare every char. Space: O(1).

Approach 2 — Horizontal scan

Compare the first two, get their prefix; compare that prefix to the third; etc.

horizontal_scan.py
def longest_common_prefix(strs):
    if not strs:
        return ""
    prefix = strs[0]
    for s in strs[1:]:
        while not s.startswith(prefix):
            prefix = prefix[:-1]
            if not prefix:
                return ""
    return prefix

Same Big-O. Slightly worse worst case due to repeated startswith. Vertical scan is the cleaner default.

Approach 3 — Sort + compare endpoints (clever, slower)

Sort the array; the first and last strings are the most "different." The common prefix of the array is the common prefix of those two.

sort_endpoints.py
def longest_common_prefix(strs):
    if not strs:
        return ""
    strs.sort()
    first, last = strs[0], strs[-1]
    i = 0
    while i < len(first) and i < len(last) and first[i] == last[i]:
        i += 1
    return first[:i]

Time: O(n log n × m) due to sort. Elegant; mention as an alternative if asked for variations.

What to say at the end

"Vertical scan in O(S) is the cleanest. Sort-and-compare-endpoints is elegant but slower; only worth it if amortized over many lookups."

2Valid Anagram

Hash TableString · LeetCode 242 · Easy

Prompt: Given two strings s and t, return True if t is an anagram of s.

examples
"anagram", "nagaram" → True
"rat", "car"         → False

Clarify: case sensitive? Unicode? Assume lowercase ASCII unless told otherwise. Different lengths → immediately False.

Approach 1 — Counter (cleanest)
counter.py
from collections import Counter

def is_anagram(s, t):
    if len(s) != len(t):
        return False
    return Counter(s) == Counter(t)

Time: O(n). Space: O(k) where k = alphabet size.

Approach 2 — Sort and compare
sort_compare.py
def is_anagram(s, t):
    return sorted(s) == sorted(t)

Time: O(n log n). One-liner. Slower in Big-O, but Python's Timsort is fast in practice.

Approach 3 — Manual count (no Counter)

If they want you to show you can do it without library help:

manual_count.py
def is_anagram(s, t):
    if len(s) != len(t):
        return False
    counts = {}
    for c in s:
        counts[c] = counts.get(c, 0) + 1
    for c in t:
        if c not in counts:
            return False
        counts[c] -= 1
        if counts[c] < 0:
            return False
    return True

Time O(n), Space O(k).

Follow-up: what about Unicode? Streaming?

Unicode: Counter and dict-based approaches still work. The fixed-size int-array optimization (int[26]) doesn't apply.

Can't fit in memory: streaming Counter (one pass each), or external sort.

3Subarray Sum Equals K

ArrayHash TablePrefix Sum · LeetCode 560 · Medium

Prompt: Given an integer array nums and integer k, return the number of contiguous subarrays whose sum equals k. Numbers may be negative.

examples
nums = [1, 1, 1], k = 2  →  2  (two [1,1] subarrays)
nums = [1, 2, 3], k = 3  →  2  ([1,2] and [3])
Common trap

Sliding window doesn't work here — nums can contain negatives, so the running sum isn't monotonic. The unlock is prefix sum + hash map.

Approach 1 — Brute force (state it, then beat it)
brute.py
def subarray_sum_brute(nums, k):
    count = 0
    for i in range(len(nums)):
        s = 0
        for j in range(i, len(nums)):
            s += nums[j]
            if s == k:
                count += 1
    return count

Time O(n²). Works for small inputs.

The insight (say this out loud before coding)

Define prefix[i] = sum of nums[0..i-1]. Then the sum of nums[i..j] equals prefix[j+1] - prefix[i].

We want subarrays where this equals k:

prefix[j+1] - prefix[i] = k
       ⇒ prefix[i] = prefix[j+1] - k

So as we walk through computing prefix sums, at each position j, we ask: "how many prior prefix sums equal (current_prefix - k)?" That count is the number of subarrays ending at j with sum k. Sum it.

Approach 2 — Optimal (prefix sum + hash map)
optimal.py
def subarray_sum(nums, k):
    count = 0
    prefix_sum = 0
    sums_seen = {0: 1}  # the "empty prefix" — see note below
    for n in nums:
        prefix_sum += n
        if prefix_sum - k in sums_seen:
            count += sums_seen[prefix_sum - k]
        sums_seen[prefix_sum] = sums_seen.get(prefix_sum, 0) + 1
    return count

Time: O(n). Space: O(n).

Why {0: 1} at the start

We initialize the map with {0: 1} to handle subarrays that start at index 0. Without it, when prefix_sum == k at some point, we'd miss counting the subarray nums[0..i]. Always say this aloud — interviewers love when you explain the empty-prefix trick.

Walk-through: [1, 1, 1], k = 2
Start:    count=0, prefix=0, seen={0:1}
See 1:    prefix=1.  prefix-k=-1 not in seen.  seen={0:1, 1:1}
See 1:    prefix=2.  prefix-k=0 in seen (count 1) → count=1.  seen={0:1, 1:1, 2:1}
See 1:    prefix=3.  prefix-k=1 in seen (count 1) → count=2.  seen={0:1, 1:1, 2:1, 3:1}
Return:   2 ✓

4Two Sum

Hash TableArray · LeetCode 1 · Easy · The universal warmup

Prompt: Given nums and target, return indices of two numbers that sum to target. Assume exactly one solution.

Solution — hash map, one pass
two_sum.py
def two_sum(nums, target):
    seen = {}                       # value → index
    for i, n in enumerate(nums):
        complement = target - n
        if complement in seen:
            return [seen[complement], i]
        seen[n] = i
    return []

Time: O(n). Space: O(n). The pattern — "look for the complement in a hash map as you iterate, store the current" — appears in dozens of problem variations.

5Group Anagrams

Hash TableStringSort · LeetCode 49 · Medium

Prompt: Group strings that are anagrams of each other.

example
["eat","tea","tan","ate","nat","bat"]
→ [["eat","tea","ate"], ["tan","nat"], ["bat"]]
Solution — defaultdict with sorted-string keys
group_anagrams.py
from collections import defaultdict

def group_anagrams(words):
    groups = defaultdict(list)
    for word in words:
        key = ''.join(sorted(word))
        groups[key].append(word)
    return list(groups.values())

Time: O(n × m log m) where m = average word length. Space: O(n × m).

Faster key: a tuple of 26 char counts gives O(n × m) but is rarely worth the complexity in an interview.

6Number of Islands

DFSGrid · LeetCode 200 · Medium · DFS classic

Prompt: Given a grid of '1' (land) and '0' (water), count connected components of land. Diagonals don't connect.

Solution — DFS, mark visited by mutating
num_islands.py
def num_islands(grid):
    if not grid: return 0
    rows, cols = len(grid), len(grid[0])
    count = 0

    def dfs(r, c):
        if r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] != '1':
            return
        grid[r][c] = '0'           # mark visited (mutates — clarify!)
        dfs(r+1, c); dfs(r-1, c); dfs(r, c+1); dfs(r, c-1)

    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == '1':
                count += 1
                dfs(r, c)
    return count

Time: O(rows × cols). Space: O(rows × cols) recursion in worst case.

If interviewer says "don't mutate input": use a visited set instead. Mention it; mutation is cleanest if allowed.

7Maximum Subarray

ArrayKadane · LeetCode 53 · Medium

Prompt: Find the contiguous subarray with the largest sum.

Solution — Kadane's algorithm
kadane.py
def max_subarray(nums):
    best = current = nums[0]
    for n in nums[1:]:
        current = max(n, current + n)
        best = max(best, current)
    return best

Time: O(n). Space: O(1).

The insight: at each position, decide — extend the previous subarray or start fresh. Whichever is bigger. current = max(n, current + n) captures both choices.

8Valid Palindrome

StringTwo Pointers · LeetCode 125 · Easy

Prompt: Determine if a string is a palindrome considering only alphanumeric characters and ignoring case.

Solution — two pointers from both ends
valid_palindrome.py
def is_palindrome(s):
    left, right = 0, len(s) - 1
    while left < right:
        while left < right and not s[left].isalnum():
            left += 1
        while left < right and not s[right].isalnum():
            right -= 1
        if s[left].lower() != s[right].lower():
            return False
        left += 1
        right -= 1
    return True

Time: O(n). Space: O(1).

9Longest Substring Without Repeating Characters

Sliding WindowHashString · LeetCode 3 · Medium

Prompt: Find the length of the longest substring with all distinct characters.

Solution — sliding window with last-seen map
longest_unique.py
def longest_unique_substring(s):
    last_seen = {}
    left = 0
    best = 0
    for right, c in enumerate(s):
        if c in last_seen and last_seen[c] >= left:
            left = last_seen[c] + 1
        last_seen[c] = right
        best = max(best, right - left + 1)
    return best

Time: O(n). Space: O(k) bounded by alphabet.

Key detail: last_seen[c] >= left. A character "reset" only matters if it's still inside the current window.

10Validate Binary Search Tree

DFSTree · LeetCode 98 · Medium

Prompt: Determine if a binary tree is a valid BST.

The naïve trap (don't do this)

Comparing each node only to node.left and node.right is wrong. A node's value must be bounded by all ancestors, not just its parent.

Solution — DFS with bounds inherited from ancestors
validate_bst.py
def is_valid_bst(root, low=float('-inf'), high=float('inf')):
    if not root:
        return True
    if not (low < root.val < high):
        return False
    return (is_valid_bst(root.left,  low,        root.val)
        and is_valid_bst(root.right, root.val,   high))

Time: O(n). Space: O(h) recursion (h = tree height).

Alternative: in-order traversal yields sorted sequence in a valid BST; check that it's strictly increasing.

11Merge Intervals

SortArray · LeetCode 56 · Medium

Prompt: Given intervals, merge all overlapping ones.

example
[[1,3],[2,6],[8,10],[15,18]] → [[1,6],[8,10],[15,18]]
Solution — sort by start, sweep
merge_intervals.py
def merge(intervals):
    if not intervals: return []
    intervals.sort(key=lambda x: x[0])
    merged = [intervals[0]]
    for start, end in intervals[1:]:
        if start <= merged[-1][1]:
            merged[-1][1] = max(merged[-1][1], end)
        else:
            merged.append([start, end])
    return merged

Time: O(n log n) from the sort. Space: O(n).

The pattern — sort by start, then sweep linearly — generalizes to "calendar" / "meeting room" / "schedule conflict" problems.

Drill protocol

For each problem:

  1. Toggle Drill mode at the top to hide solutions.
  2. Set a timer — 25 min for first pass, 10 min for re-solves.
  3. Code from scratch. Talk out loud (yes, even alone — builds the muscle).
  4. State complexity at the end.
  5. Reveal the solution. Note differences.
  6. Mark as practiced. Re-solve from memory the next day.

You're not memorizing — you're building pattern recognition. When you see "find subarrays with sum X" your brain should fire prefix sum + hash map before you reach for a brute force.