Simulation and Stateful Iteration
Some problems have no trick. The statement describes a process, and the task is to execute it exactly, without an off-by-one or a missed case. Interviewers use these deliberately: they measure whether you can hold several pieces of state straight and translate a specification faithfully — which is most of real engineering.
There are still techniques, and they are worth having ready:
- Direction vectors instead of four hard-coded branches.
- In-place state encoding when reads and writes collide.
- Recognising when not to simulate — the most valuable one, because some problems describe a billion steps and expect you to reason instead.
What you’ll learn
- Rotating a direction in one line, with no
ififchain. - The two-bit trick for simultaneous updates in space.
- Bouncing state machines — the
stepstepvariable that replaces modular index arithmetic. - The cycle argument: why four iterations settle a rotation question forever.
- Three real LeetCode problems solved in the browser: 289, 1041, 6.
The cue
Trick 1 — direction vectors
Four ifif branches for up/down/left/right is where bugs live. A vector list plus an
index is shorter and rotates in one line:
# clockwise order: north, east, south, west
DIRS = ((0, 1), (1, 0), (0, -1), (-1, 0))
facing = 0 # index into DIRS
facing = (facing + 1) % 4 # turn RIGHT
facing = (facing - 1) % 4 # turn LEFT (Python's % keeps it non-negative)
dx, dy = DIRS[facing]
x, y = x + dx, y + dy # step forward# clockwise order: north, east, south, west
DIRS = ((0, 1), (1, 0), (0, -1), (-1, 0))
facing = 0 # index into DIRS
facing = (facing + 1) % 4 # turn RIGHT
facing = (facing - 1) % 4 # turn LEFT (Python's % keeps it non-negative)
dx, dy = DIRS[facing]
x, y = x + dx, y + dy # step forwardKeeping the directions in clockwise order is what makes +1+1 a right turn.
Python’s modulo returning a non-negative result for a positive divisor means
(0 - 1) % 4 == 3(0 - 1) % 4 == 3 — so turning left from index 0 correctly gives west, with no
special case. In C or Java that expression yields -1-1 and needs + 4+ 4 first.
An equally clean alternative avoids the index entirely:
dx, dy = -dy, dx # rotate LEFT 90 degrees
dx, dy = dy, -dx # rotate RIGHT 90 degreesdx, dy = -dy, dx # rotate LEFT 90 degrees
dx, dy = dy, -dx # rotate RIGHT 90 degreesWorth memorising — it is two characters of difference, and getting the pair backwards mirrors the whole simulation.
Trick 2 — encode two states in one cell
Game of Life updates every cell based on its neighbours’ current values. Write as you read and you corrupt the input mid-pass.
The -space fix stores both states in the same integer: bit 0 is the present, bit 1 is the future.
def game_of_life(board):
rows, cols = len(board), len(board[0])
for r in range(rows):
for c in range(cols):
live = 0
for dr in (-1, 0, 1):
for dc in (-1, 0, 1):
if dr == 0 and dc == 0:
continue
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols:
live += board[nr][nc] & 1 # read ONLY bit 0
# write the next state into bit 1
if board[r][c] & 1:
if live in (2, 3):
board[r][c] |= 2
elif live == 3:
board[r][c] |= 2
for r in range(rows): # second pass: shift
for c in range(cols):
board[r][c] >>= 1
return boarddef game_of_life(board):
rows, cols = len(board), len(board[0])
for r in range(rows):
for c in range(cols):
live = 0
for dr in (-1, 0, 1):
for dc in (-1, 0, 1):
if dr == 0 and dc == 0:
continue
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols:
live += board[nr][nc] & 1 # read ONLY bit 0
# write the next state into bit 1
if board[r][c] & 1:
if live in (2, 3):
board[r][c] |= 2
elif live == 3:
board[r][c] |= 2
for r in range(rows): # second pass: shift
for c in range(cols):
board[r][c] >>= 1
return boardTrick 3 — bouncing state
For zigzag traversal (LC 6), the row index goes down then up then down. Modular arithmetic on the cycle length works but is fiddly; a step variable that flips at the boundaries is much clearer:
def convert(s, num_rows):
if num_rows == 1:
return s # no zigzag exists; also avoids a hang
rows = [[] for _ in range(num_rows)]
row, step = 0, 1
for ch in s:
rows[row].append(ch)
if row == 0:
step = 1 # bounce off the top
elif row == num_rows - 1:
step = -1 # bounce off the bottom
row += step
return "".join("".join(r) for r in rows)def convert(s, num_rows):
if num_rows == 1:
return s # no zigzag exists; also avoids a hang
rows = [[] for _ in range(num_rows)]
row, step = 0, 1
for ch in s:
rows[row].append(ch)
if row == 0:
step = 1 # bounce off the top
elif row == num_rows - 1:
step = -1 # bounce off the bottom
row += step
return "".join("".join(r) for r in rows)Trick 4 — do not simulate
This is the most valuable habit on the page.
LC 1041 asks whether a robot stays within a bounded circle when its instruction string repeats forever. Simulating forever is impossible. The insight:
After four repetitions, the robot’s net rotation is a multiple of 360°, so its displacement pattern repeats. So it is bounded if and only if, after one pass, either it is back at the origin or it is no longer facing its original direction.
If the robot ends facing north again (unrotated) at a non-origin position, each repetition adds the same displacement and it escapes. If it has rotated at all, four passes compose into a closed loop and it is bounded.
def is_robot_bounded(instructions):
x = y = 0
dx, dy = 0, 1 # facing north
for ch in instructions:
if ch == "G":
x, y = x + dx, y + dy
elif ch == "L":
dx, dy = -dy, dx
else: # "R"
dx, dy = dy, -dx
return (x == 0 and y == 0) or (dx, dy) != (0, 1)def is_robot_bounded(instructions):
x = y = 0
dx, dy = 0, 1 # facing north
for ch in instructions:
if ch == "G":
x, y = x + dx, y + dy
elif ch == "L":
dx, dy = -dy, dx
else: # "R"
dx, dy = dy, -dx
return (x == 0 and y == 0) or (dx, dy) != (0, 1)One pass, , no loop over repetitions at all.
| Problem | Naive | Better |
|---|---|---|
| 289 Game of Life | space copy | space via bit encoding |
| 1041 Robot Bounded | simulate forever | one pass + a rotation argument |
| 6 Zigzag | build the full grid, | bucket per row, |
The variant map
| Variant | The technique | Canonical problem |
|---|---|---|
| Cellular update | Encode next state in spare bits | 289 |
| Robot with turns | Direction vectors or the rotation pair | 1041 · 874 · 657 |
| Unusual output order | Bucket by target row/column | 6 · 54 · 498 |
| Formatting / justification | Careful integer division of spare space | 68 · 12 · 273 |
| Enormous step count | Find the cycle, then use modular arithmetic | 1041 · 957 |
| Obstacle-aware movement | A setset of obstacles for lookup | 874 |
Practice — real LeetCode problems
LC 289 — Game of Life · Medium
Problem. Given an m x nm x n board of 00 (dead) and 11 (live), compute the next
state. Each cell’s fate depends on its eight neighbours: a live cell survives
with 2 or 3 live neighbours; a dead cell becomes live with exactly 3. All updates
happen simultaneously.
Constraints. 1 <= m, n <= 251 <= m, n <= 25, cells are 00 or 11.
Examples. [[0,1,0],[0,0,1],[1,1,1],[0,0,0]][[0,1,0],[0,0,1],[1,1,1],[0,0,0]] becomes
[[0,0,0],[1,0,1],[0,1,1],[0,1,0]][[0,0,0],[1,0,1],[0,1,1],[0,1,0]] · [[1,1],[1,0]][[1,1],[1,0]] becomes [[1,1],[1,1]][[1,1],[1,1]]
Editorial — approach, complexity, follow-ups
The requirement is simultaneous update, so the naive in-place loop is wrong: a cell updated early changes what its neighbours observe.
Encoding both states in one integer resolves it. & 1& 1 reads the original value,
|= 2|= 2 writes the next one, and nothing reads bit 1 during the first pass. A second
pass shifts every value right, promoting the future to the present.
Time . Space .
[[1]][[1]] becoming [[0]][[0]] is worth checking: a single live cell has zero live
neighbours, so it dies of underpopulation. It is easy to assume a lone cell
persists.
The two-pass structure is what makes this correct rather than clever — and the same “defer the write to a place the reader ignores” idea appears in Set Matrix Zeroes, where the borders store the flags.
State the -space copy first: build a fresh board and read the original. It is simpler, obviously correct, and often all that is wanted. Offer the bit trick as the answer.
Follow-uups you should expect:
- “What if the board is infinite?” LeetCode asks this directly. Store only the
live cells as a
setsetof coordinates, and for each iteration consider live cells plus their neighbours — work proportional to the live population, not the grid. That is the real-world answer. - “What if it does not fit in memory?” Process in horizontal bands, keeping the boundary rows of adjacent bands so neighbour counts stay correct.
- “More than two states?” Bit encoding still works with more bits, or use larger sentinel values.
- “Why not update in place naively?” Show a concrete cell whose neighbour count changes mid-pass.
LC 1041 — Robot Bounded In Circle · Medium
Problem. A robot starts at (0, 0)(0, 0) facing north and repeats an instruction
string forever. "G""G" moves forward one unit, "L""L" turns left 90°, "R""R" turns
right 90°. Return TrueTrue if there is a circle that the robot never leaves.
Constraints. 1 <= len(instructions) <= 1001 <= len(instructions) <= 100, characters are GG, LL or RR.
Examples. "GGLLGG""GGLLGG" gives TrueTrue (returns to the origin) ·
"GG""GG" gives FalseFalse (walks north forever) · "GL""GL" gives TrueTrue
Editorial — approach, complexity, follow-ups
Simulating “forever” is impossible, so reason about what one pass does.
After one pass the robot has a net displacement and a net rotation. Two cases:
- Net rotation is zero (still facing north). Every pass then adds the same displacement, so unless that displacement is zero the robot drifts away forever.
- Net rotation is non-zero (90°, 180° or 270°). Four passes compose to a multiple of 360°, and the four displacement vectors — each rotated relative to the last — sum to zero. The robot returns to the origin every four passes and is therefore bounded.
So the test is: back at the origin, or not facing the original direction.
Time , one pass. Space .
The test cases cover each branch:
"GG""GG"givesFalseFalse— no rotation, non-zero displacement; the only unbounded shape."GL""GL"givesTrueTrue— rotated, so bounded even though it moved."GGLLGG""GGLLGG"givesTrueTrue— returns to the origin exactly."GLGLGGLGL""GLGLGGLGL"givesFalseFalse— the case worth tracing; it ends facing north with a non-zero displacement despite containing turns. Net rotation is what matters, not the presence of turns."R""R"givesTrueTrue— rotation with no movement at all."GLRLLG""GLRLLG"givesTrueTrue— turns that partially cancel, still leaving a net rotation.
Getting the two rotation formulas the right way round matters. Left is
(-dy, dx)(-dy, dx), right is (dy, -dx)(dy, -dx). Swapping them mirrors every simulation, and
several of these cases would still pass by symmetry — which makes it a nasty bug.
"GLGLGGLGL""GLGLGGLGL" is the one that discriminates.
Follow-ups you should expect: “Why four passes?” — 90° rotations have order 4,
so the composition closes. “What if turns were 60°?” — then six passes; the
argument generalises to any rational rotation. “Track the actual bounding radius?”
— accumulate the maximum distance over four passes. “Walking Robot Simulation
(LC 874)?” — genuine step-by-step simulation, with obstacles in a setset for
lookup.
LC 6 — Zigzag Conversion · Medium
Problem. Write the string ss in a zigzag pattern over numRowsnumRows rows, then
read it off row by row.
Constraints. 1 <= len(s) <= 10001 <= len(s) <= 1000, 1 <= numRows <= 10001 <= numRows <= 1000, ss is letters,
,, and ...
Examples. "PAYPALISHIRING", 3"PAYPALISHIRING", 3 gives "PAHNAPLSIIGYIR""PAHNAPLSIIGYIR" ·
"PAYPALISHIRING", 4"PAYPALISHIRING", 4 gives "PINALSIGYAHRPI""PINALSIGYAHRPI" · "A", 1"A", 1 gives "A""A"
Editorial — approach, complexity, follow-ups
You never need the grid — only which row each character lands in. Walk the string once, appending to the current row’s bucket, and flip direction at the boundaries.
Time . Space for the output.
Three details:
numRows == 1numRows == 1must be guarded. With one row,row == 0row == 0androw == numRows - 1row == numRows - 1are both true. Theif row == 0if row == 0branch wins, setsstep = 1step = 1, androwrowbecomes11— out of range on the next append. Returningssunchanged is both safe and correct.- Check
row == 0row == 0first. The ordering resolves the ambiguous single-row case deterministically, which is what makes the guard sufficient. numRowsnumRowsmay exceed the string length.("AB", 5)("AB", 5)gives"AB""AB": the walk never reaches the bottom, sostepstepstays11, characters land in rows 0 and 1, and the empty rows contribute nothing to the join. No special handling needed.
("ABCD", 2)("ABCD", 2) giving "ACBD""ACBD" is the smallest genuine zigzag: rows are A, CA, C and
B, DB, D.
Building a list of lists and joining is markedly faster in Python than repeated string concatenation, which is per operation because strings are immutable.
The alternative approach computes each row’s indices directly with the cycle length
2 * numRows - 22 * numRows - 2 and a formula per row. It is too and avoids the state
variable, but the index arithmetic for the middle rows — which receive two
characters per cycle — is easy to get wrong under pressure. The bouncing step is
harder to break.
Follow-ups you should expect: “Do it with index arithmetic instead?” — the cycle-length formula; mention the two-per-cycle subtlety for middle rows. “Reverse the transformation?” — decode by reconstructing the same row assignment and reading back. “Why not a 2D grid?” — space, mostly empty. “Text Justification (LC 68)?” — the same genre, harder: distribute spare spaces left-to-right with integer division, and left-justify the final line.
LeetCode problem set
| # | Problem | Difficulty | The twist |
|---|---|---|---|
| 657 | Robot Return to Origin | Easy | Just count opposing moves; no simulation needed |
| 6 | Zigzag Conversion | Medium | Bouncing step; guard numRows == 1numRows == 1 |
| 289 | Game of Life | Medium | Two-bit encoding for simultaneous in-place update |
| 1041 | Robot Bounded In Circle | Medium | One pass + a rotation argument — do not simulate forever |
| 874 | Walking Robot Simulation | Medium | Genuine simulation; obstacles in a setset for lookup |
| 68 | Text Justification | Hard | Pure specification-following; the last line is left-justified |
Interview follow-ups
| They ask | What they’re checking | The answer |
|---|---|---|
| “Should you simulate at all?” | The most important judgement | Not when the step count is huge — find a cycle, a closed form, or an invariant |
| “How do you handle simultaneous updates?” | The in-place trick | Encode the next state in spare bits, read with & 1& 1, write with |= 2|= 2, then shift |
| “How do you rotate a direction?” | Fluency | (-dy, dx)(-dy, dx) for left, (dy, -dx)(dy, -dx) for right; or a clockwise DIRSDIRS list with ± 1 mod 4± 1 mod 4 |
| “Why four passes for LC 1041?” | The cycle argument | 90° rotations have order 4, so four passes compose to a closed loop |
| “What if the board were infinite?” | Real-world scaling | Store only live cells in a setset; work scales with the live population |
| “Why not build the 2D grid for zigzag?” | Space awareness | and mostly empty; buckets per row are |
| “How do you avoid off-by-ones here?” | Method | Enumerate the boundary cases before coding, and pick the formulation with fewer branches |
Edge-case checklist
- Single cell / single character —
[[1]][[1]]dies;"A""A"with 1 row returns itself. numRows == 1numRows == 1(LC 6) — must be guarded or it raises.numRowsnumRowsgreater thanlen(s)len(s)—("AB", 5)("AB", 5)gives"AB""AB"; empty rows contribute nothing.- A lone live cell (LC 289) — dies of underpopulation; not obviously so.
- All cells dead / all alive — the trivial extremes.
- Instructions with no
GG(LC 1041) —"R""R"givesTrueTrue, never moving. - Instructions with no turns —
"GG""GG"givesFalseFalse, the only unbounded shape. - Turns that cancel to zero net rotation —
"GLGLGGLGL""GLGLGGLGL"givesFalseFalse; net rotation is what counts, not the presence of turns. - Left/right rotation formulas swapped — mirrors everything, and several cases still pass by symmetry.
- Boundary reads (LC 289) — corner cells have only 3 neighbours; bound-check every offset.
Recap
- Some problems have no trick, and the skill is faithful translation of the specification plus disciplined case enumeration.
- Direction vectors or the rotation pair (
(-dy, dx)(-dy, dx)left,(dy, -dx)(dy, -dx)right) replace four-branchififchains. Python’s non-negative%%makes(i - 1) % 4(i - 1) % 4safe. - For simultaneous updates, encode the next state in spare bits — read with
& 1& 1, write with|= 2|= 2, then shift. Offer the copy first; the bit trick is the follow-up. - A bouncing step variable beats modular index arithmetic for zigzag traversal
— and
numRows == 1numRows == 1must be guarded explicitly. - The most valuable instinct is not simulating. When the step count is huge but the state space is small, find the cycle and reduce modulo its length. LC 1041 needs one pass and a rotation argument; LC 957 needs cycle detection.
- Build strings by joining a list, never by repeated concatenation.
Next: the advanced graph algorithms — MST, strongly connected components, and max flow.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
