Lab 03 — Debugging Under Pressure (Word Break with a Planted Bug)
Goal
Build a systematic debugging protocol you can execute under interview pressure in under 5 minutes. The anchor is Word Break (LC 139) with a planted off-by-one — you’ll be given buggy code and a failing test, and the goal is to find the bug methodically rather than by panic-staring at the screen. By the end you should reach for the protocol automatically when stuck.
Background Concepts
Under pressure, most candidates default to panic debugging: re-read the code 5 times, add random print statements, change one thing, hope. This rarely works in 5 minutes and looks terrible to the interviewer.
Systematic debugging is a 6-step protocol:
- Reproduce — Confirm the failing input. The smallest one that fails.
- Isolate — What is the exact discrepancy? Expected X, got Y.
- Hypothesize — Form a specific hypothesis: “I think the bug is in the inner loop’s bound.”
- Verify — Add one targeted print or assertion that confirms or denies the hypothesis. Not five prints.
- Fix — Make the minimum change that addresses the verified hypothesis.
- Re-test — Run all tests, not just the failing one. Make sure you didn’t break something else.
The discipline is in steps 3 and 4. Hypothesize before you print. Random prints waste time and create noise.
Interview Context
You will hit a bug in 80% of medium+ interview problems. How you respond is a major signal:
| Behavior | Signal |
|---|---|
| “It doesn’t work, let me try…” (silent typing for 3 min) | Junior — no protocol |
| “Let me add some prints…” (adds 8 prints, can’t read output) | Junior — random debugging |
“The expected output is X, I got Y. So the difference is Z. My hypothesis is that the bug is in the loop bound — let me check by printing i at line 7.” | Senior — narrating the protocol |
| Finds the bug, then says “Let me also test the case I just fixed plus an adjacent case, in case I introduced something.” | Senior+ — proactive regression |
Narrating the protocol aloud is itself the signal. The interviewer can hear that you’re a person who has debugged a thousand bugs and has a process.
Problem Statement
You are given the following implementation of Word Break. It is buggy. Find and fix the bug using the debug protocol. You may add prints/asserts but must remove them before declaring the fix complete.
def word_break(s: str, word_dict: list[str]) -> bool:
"""Return True iff s can be segmented into a sequence of words from word_dict."""
words = set(word_dict)
n = len(s)
# dp[i] = True iff s[:i] can be segmented
dp = [False] * n
dp[0] = True
for i in range(1, n + 1):
for j in range(i):
if dp[j] and s[j:i] in words:
dp[i] = True
break
return dp[n]
Failing test: word_break("leetcode", ["leet", "code"]) should return True. The function raises IndexError.
Constraints
- 1 ≤ |s| ≤ 300
- 1 ≤ |word_dict| ≤ 1000
- All strings lowercase letters
Clarifying Questions
(Not applicable — this lab uses a fixed buggy implementation. The clarifying questions for Word Break itself are: are words reusable? Yes. Are duplicates in word_dict significant? No. Empty s? Returns True conventionally.)
Examples
word_break("leetcode", ["leet", "code"]) → True
word_break("applepenapple", ["apple", "pen"]) → True
word_break("catsandog", ["cats", "dog", "sand", "and", "cat"]) → False
word_break("", ["any"]) → True (empty string is trivially segmentable)
Initial Brute Force
Recursion: for each prefix of s that is in word_dict, recurse on the suffix. Time O(2^N) without memoization.
Brute Force Complexity
O(2^N) without memoization; O(N²) with (each prefix length tried once, each requiring an O(N) substring check and set lookup).
Optimization Path
Bottom-up DP: dp[i] = True iff s[:i] can be segmented. Transition: dp[i] = True iff there exists j < i with dp[j] True and s[j:i] ∈ words. The buggy implementation above is the right idea — just slightly wrong.
Final Expected Approach (Correct Version)
def word_break(s, word_dict):
words = set(word_dict)
n = len(s)
dp = [False] * (n + 1) # ← THE FIX: size n+1, not n
dp[0] = True
for i in range(1, n + 1):
for j in range(i):
if dp[j] and s[j:i] in words:
dp[i] = True
break
return dp[n]
Applying the Debug Protocol (Walkthrough)
Step 1 — Reproduce
Run word_break("leetcode", ["leet", "code"]). Get IndexError: list assignment index out of range. Confirm with the exact line: the assignment dp[i] = True when i = n.
Step 2 — Isolate
The loop runs i from 1 to n inclusive (range(1, n + 1)). dp has length n. So when i == n, dp[i] is out of bounds.
Step 3 — Hypothesize
The DP array is one element too small. The semantics of dp[i] cover i = 0 (empty prefix) through i = n (full string), which is n + 1 values. The author wrote dp = [False] * n — off by one.
Step 4 — Verify
Add assert len(dp) == n + 1 after allocation. It fails, confirming the hypothesis.
Step 5 — Fix
Change dp = [False] * n to dp = [False] * (n + 1).
Step 6 — Re-test
Run the failing test → True. Run all four examples → all pass. Add an edge test word_break("", []) → returns True (since dp[0] is True initialized, and dp[n] == dp[0]).
Total time if narrated cleanly: under 4 minutes.
Data Structures Used
- Set for O(1) word lookup
- DP array of booleans
Correctness Argument
dp[0] is the base case: the empty prefix is trivially segmentable. For i > 0, dp[i] is True iff some split point j makes both halves valid: dp[j] (the left half is segmentable) and s[j:i] is a word. By induction on i, this is correct.
Complexity
Time O(N²) (or O(N² + total dict string length) if you care about set membership cost). Space O(N + W) where W is the dictionary size.
Implementation Requirements
- DP array size must be
n + 1, notn— this is the planted bug - Use a set for word lookup, not a list (O(1) vs O(W) per check)
- Break out of inner loop on first success (constant factor, not asymptotic)
Tests
Smoke (3 normal)
assert word_break("leetcode", ["leet", "code"]) is True
assert word_break("applepenapple", ["apple", "pen"]) is True
assert word_break("catsandog", ["cats", "dog", "sand", "and", "cat"]) is False
Edge
assert word_break("", ["any"]) is True # empty string
assert word_break("a", ["a"]) is True # single char match
assert word_break("a", ["b"]) is False # single char no match
assert word_break("aaaa", ["a", "aa"]) is True # overlapping dict
assert word_break("ab", ["a"]) is False # partial match
Large
s = "a" * 300
dict_ = ["a", "aa", "aaa", "aaaa"]
assert word_break(s, dict_) is True
# Should complete in <100ms; O(N^2) = 9e4 ops
Randomized
import random, string
random.seed(0)
for _ in range(100):
words = [''.join(random.choices(string.ascii_lowercase, k=random.randint(1, 4)))
for _ in range(5)]
s = ''.join(random.choice(words) for _ in range(random.randint(1, 10)))
assert word_break(s, words) is True # constructed to be segmentable
Invalid input
# Empty dict — empty string still works, non-empty doesn't
assert word_break("", []) is True
assert word_break("a", []) is False
Follow-up Questions
- Return all segmentations. Word Break II (LC 140). DFS + memoization; exponentially many results possible.
- Return one segmentation. Track parent pointers in DP; reconstruct by backtracking.
- Minimum number of words. Modify DP:
dp[i] = min over j of dp[j] + 1. - Streaming input. Word Break on a stream — Aho-Corasick automaton.
- Dictionary changes dynamically. Trie + DP, but rebuilds are expensive on every dict change.
Product Extension
- Spell-check / autocomplete segmentation. “iphoneapp” → “iphone app”. Used in URL/path tokenization.
- Hashtag splitting. Twitter “#machinelearning” → “machine learning”. Same algorithm with a dictionary + frequency weights for tiebreaks.
- DNS subdomain analysis. “thequickbrownfox.com” — fraud detection wants to know if the hostname is composed of dictionary words.
- Chinese/Japanese word segmentation. No spaces between words; same DP with a much larger dictionary.
Language/Runtime Follow-ups
- Python:
s[j:i]allocates a new string each call (O(i-j) time and space). For very long strings, prefer indexing into a precomputed structure or usingstr.startswithagainst the dictionary entries. - Java:
s.substring(j, i)is O(i-j) since Java 7 (used to be O(1) view; changed for security). Same allocation cost. - Go:
s[j:i]on a string is a view — O(1), no allocation. This makes Go’s version significantly faster. - C++:
std::string_view(C++17) gives O(1) slicing;s.substr(j, i-j)allocates. - All: Set membership of strings is O(|substring|) for hashing + O(|substring|) for equality on collision — not strictly O(1). Matters for very long substrings.
Common Bugs
- Off-by-one on DP size — the planted bug
- Initializing
dp[0] = False— empty prefix is the base case, must be True - Forgetting
breakafter success — correctness still works but performance suffers - Using list for word_dict — O(W) per lookup, blows complexity to O(N²·W)
- Inclusive vs exclusive bounds —
s[j:i+1]vss[j:i]is the most common off-by-one when porting between languages
Debugging Strategy
The 5-step systematic protocol:
- Reproduce minimally. If the bug shows up on a 300-char string, shrink to the smallest failing case (here: any non-empty string).
- Read the exception fully.
IndexError+ line number tells you almost everything. Don’t skip the stack trace. - State the discrepancy precisely. “Expected True, got IndexError on
dp[i] = Truewheni = 8andlen(dp) = 8.” - Form a specific hypothesis. Not “it’s broken somewhere”; rather, “the array is one too small for the loop range.”
- Verify with one targeted print/assert.
assert len(dp) > iimmediately before the assignment. - Regression test. After fixing, run the full suite. In this case, also test empty string explicitly since that’s the boundary.
The 5-minute panic protocol (when truly stuck):
- Stop typing.
- State aloud: “I’m stuck. Let me restate what I know.”
- Restate the input and expected output.
- State what your code does for that input, step by step.
- The bug almost always reveals itself in the gap between “what the code does” and “what should happen.”
Mastery Criteria
- Found the planted bug in under 5 minutes using the protocol
- Narrated each step aloud (or in notes) — Reproduce, Isolate, Hypothesize, Verify, Fix, Re-test
- Added exactly ONE targeted assertion to verify the hypothesis (not 5 prints)
- Ran the full test suite after the fix, not just the failing one
- Wrote the empty-string edge test that would have caught this bug originally
- Can recite the 6-step protocol from memory
- Applied the protocol to one of your own past wrong-answer submissions and timed yourself