Construction and Exploration
What is this?
Choose, recurse, undo. Every problem here uses that same skeleton, and every one is decided by something else: what you know before the recursion starts. A validity rule that kills a segment immediately. A prefix map that offers only words which can legally follow. A union-find that groups synonyms so thoroughly there is barely a search left. And one problem where you cannot see the input at all.
segment > 255 or leading zero"] B --> D["Word Squares
prefix map → legal words only"] B --> E["Synonymous Sentences
union-find first, then a product"] B --> F["Robot Room Cleaner
visited set of relative coords"] F --> G["undo = turn, move, turn back"]
💡 Fun fact: Word Squares has a constraint most people miss on the first read — the square must read the same across and down, which means that once
krows are placed, rowkis completely determined in its firstkcharacters: they are thek-th letters of the rows above. So the candidate list is not "all words", it is "all words with this exact prefix". Precomputing a prefix → words map turns an intractable search into one that finishes instantly, and the map is four lines.
🔓 The 4 problems in this chapter are free. Sign in with Google or Microsoft to start solving.
The one-line idea: write the recursion in thirty seconds, then spend the rest of your time on the cheapest test that proves a branch is dead — that test is the solution.
1. Reject per segment, not per candidate
def bt(i, parts):
if len(parts) == 4:
if i == len(s): out.append(".".join(parts))
return
for L in (1, 2, 3):
if i + L > len(s): break
seg = s[i:i+L]
if (seg[0] == '0' and L > 1) or int(seg) > 255: continue # dead now
bt(i + L, parts + [seg])
Both rejections happen the instant a segment is formed, so no branch survives to depth four just to be discarded. The leading-zero rule is the one people forget: "0" is valid, "01" is not, and it costs a single clause.
The tree has at most three branches per level over four levels, so this is O(1) regardless of input length — worth saying, because it sounds wrong and is not.
2. Precompute the legal candidates
pref = defaultdict(list)
for w in words:
for i in range(n + 1):
pref[w[:i]].append(w) # every prefix maps to the words holding it
def bt(square):
if len(square) == n: out.append(square[:]); return
k = len(square)
p = "".join(w[k] for w in square) # column k of the rows already placed
for cand in pref[p]: # only words that can legally follow
square.append(cand); bt(square); square.pop()
The line that carries everything is p: reading down column k of the placed rows gives the prefix row k must start with. Without the map you would filter the whole word list at every node; with it, you index straight to the survivors.
A trie is the textbook alternative and is asymptotically the same. The hash map is shorter to write and easier to explain under time pressure — mention both, pick one, move on.
3. Group first, and the search dissolves
Synonym pairs are transitive — if a ~ b and b ~ c then all three are interchangeable — so union-find gives the true equivalence classes. After that, each word in the sentence contributes either one option or its whole sorted group, and the answer is their product in order. Sorting each group is what makes the output lexicographic without a final sort of the (potentially enormous) result.
4. Explore what you cannot see
def bt(r, c, d):
clean(); seen.add((r, c))
for k in range(4):
nd = (d + k) % 4
nr, nc = r + dr[nd], c + dc[nd]
if (nr, nc) not in seen and move():
bt(nr, nc, nd)
go_back() # turnRight ×2, move, turnRight ×2
turnRight()
Coordinates are tracked relative to the start because there is no origin to ask about. move() returning false means a wall, which is how the room is discovered. And the undo is physical: turn around, move one, turn around again — restoring both position and heading, since a robot that returns facing the wrong way corrupts every subsequent direction.
5. A 30-second worked example (IP addresses)
s = "101023"
"1" · "0" · "10" · "23" ✓ 1.0.10.23
"1" · "0" · "102" · "3" ✓ 1.0.102.3
"10" · "1" · "0" · "23" ✓ 10.1.0.23
"10" · "10" · "2" · "3" ✓ 10.10.2.3
"101" · "0" · "2" · "3" ✓ 101.0.2.3
pruned immediately:
"1" · "01" · … ✗ leading zero
"10" · "10" · "23" · "" ✗ fourth segment empty
Five valid addresses. Note "1" · "01" dies at depth two — it is never extended, which is the whole point. A version that assembles all four segments first and validates at the end explores several times as many nodes for the same answer.
6. Where you'll actually meet this
- Network tooling. Parsing and validating address strings under strict format rules.
- Robot vacuums and coverage. Blind exploration with relative odometry is the real algorithm, not a metaphor.
- Crossword and puzzle construction. Word squares are constraint satisfaction at small scale.
- Search query expansion. Expanding a phrase across synonym groups is the sentence problem exactly.
- Configuration solvers. Choose–recurse–undo with early rejection is how package and build solvers explore.
7. Problems in this chapter
▶ Restore IP Addresses
Every valid IP address formable from a digit string. Reject a segment the moment it exceeds 255 or carries a leading zero.
Pattern: backtracking with per-segment validation. Target: O(1) — the tree is bounded at 3⁴.
▶ Word Squares
All squares reading identically across and down. Build a prefix → words map, then place rows against the prefix the placed columns dictate.
Pattern: backtracking + precomputed candidates. Target: O(n · 26^L) worst case, far less in practice.
▶ Robot Room Cleaner
Clean an unknown room through a move API. Track relative coordinates, mark visited, and undo each move with a physical go-back routine.
Pattern: blind DFS with state restoration. Target: O(cells) time and space.
▶ Synonymous Sentences
All sentences formed by substituting synonyms, in lexicographic order. Union-find the synonym pairs, sort each group, then take the ordered product.
Pattern: union-find + enumeration. Target: output-bound.
8. Common pitfalls 🚫
- Validating only at the leaf. Rejecting a segment on creation is the entire optimisation.
- Allowing leading zeros.
"0"is a valid segment,"01"is not. - Filtering the whole word list at each node instead of indexing a prefix map.
- Forgetting that word squares must read both ways, which is what determines the required prefix.
- Restoring position but not heading in the robot's go-back routine.
- Treating synonyms as pairs rather than as classes. The relation is transitive.
- Sorting the final result in the sentence problem, when sorting each small group beforehand is both cheaper and sufficient.
9. Key takeaways
- The recursion is boilerplate. The pruning is the algorithm.
- Reject at the earliest provable death, not at the leaf.
- A prefix map converts "try everything" into "try what can work" — and it is four lines.
- Undo is not always an assignment. Sometimes it is four API calls, and both position and heading must return.
- Preprocessing can remove the search. Union-find turns one of these into a product.
- A bounded tree means
O(1)— say it, even when it sounds wrong. - Why interviewers like it: everyone writes the same recursion, so the pruning is the only thing that distinguishes answers.
Order: Restore IP Addresses → Word Squares → Robot Room Cleaner → Synonymous Sentences.
Robot Room Cleaner
Core idea: You're handed a robot with only four physical actions —
move(),turnLeft(),turnRight(),clean()— on a grid you cannot see and whose coordinates you are never told. There's nogetPosition(), no map, nothing. The robot doesn't know where it is, and neither do you. The whole trick is that you invent your own coordinate frame: you decree the robot starts at virtual(0, 0)facing virtual "up," and from then on you simulate its position in your head every time you callmove()or turn. Avisitedset of these virtual coordinates stops you re-cleaning. Then it's plain DFS backtracking over the four neighbors — but with one hard part the easy grid-DFS never has: when you finish exploring a branch, the real robot is physically standing in the explored cell, so before trying the next direction you must drive it back to where it was with a precise go-back maneuver (turn 180°,move()one step, turn 180° again) so the real robot's position matches your virtual model. Get the bookkeeping between relative turns (what the robot does) and absolute directions (what your model tracks) right, and the rest is standard backtracking.
The problem, rephrased
Imagine a Roomba dropped into an unknown room somewhere on a tiled floor. Some tiles are open floor; some are obstacles (furniture, walls). The Roomba has no GPS, no camera, no floor plan. All it can do is: try to roll forward one tile, pivot left a quarter-turn in place, pivot right a quarter-turn in place, and run its brush on the current tile. When it tries to roll forward, it either succeeds (the tile ahead was open, and it's now standing there) or bumps (the tile ahead was an obstacle or the room's edge, and it stayed put). Your job: drive it so that every open tile reachable from the start gets cleaned — and you must do it knowing nothing about the room's shape or your absolute position.
Formally, you control a robot object exposing exactly this API (note: no coordinates anywhere):
robot.move()→bool. Try to step one cell forward in the current facing. ReturnsTrueand moves if the next cell is open; returnsFalseand does not move if it's an obstacle or out of bounds.robot.turnLeft()/robot.turnRight()→ pivot 90° in place (same cell, new facing).robot.clean()→ clean the current cell.
The robot starts on an open cell. Clean every cell reachable (4-directionally, through open cells) from the start. All cells outside the room are treated as obstacles, so the room is fully enclosed.
This is LeetCode 489 — Robot Room Cleaner.
| The API call | What it does (no coordinates given) | Result |
|---|---|---|
robot.move() |
Step forward if the cell ahead is open | True (now in the next cell) or False (bumped, stayed) |
robot.turnLeft() |
Pivot 90° counter-clockwise, same cell | (no return; facing changed) |
robot.turnRight() |
Pivot 90° clockwise, same cell | (no return; facing changed) |
robot.clean() |
Clean the cell currently under the robot | (no return; cell marked clean) |
The defining twist versus an ordinary "flood-fill a grid" problem: you never receive (row, col). The grid exists, but you can only infer your location by faithfully tracking every move and turn yourself.
Sign in to continue reading
The rest of this lesson is available with a free account. Signing in with Google or Microsoft is free.
Sign in to read the full lesson