Skip to content

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 ifif chain.
  • The two-bit trick for simultaneous updates in O(1)O(1) space.
  • Bouncing state machines — the stepstep variable 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:

direction_vectors.py
# 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
direction_vectors.py
# 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

Keeping 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:

rotate_in_place.py
dx, dy = -dy, dx      # rotate LEFT  90 degrees
dx, dy = dy, -dx      # rotate RIGHT 90 degrees
rotate_in_place.py
dx, dy = -dy, dx      # rotate LEFT  90 degrees
dx, dy = dy, -dx      # rotate RIGHT 90 degrees

Worth 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 O(1)O(1)-space fix stores both states in the same integer: bit 0 is the present, bit 1 is the future.

game_of_life_bits.py
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 board
game_of_life_bits.py
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 board

Trick 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:

zigzag.py
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)
zigzag.py
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.

robot_bounded.py
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)
robot_bounded.py
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, O(n)O(n), no loop over repetitions at all.

ProblemNaiveBetter
289 Game of LifeO(mn)O(mn) space copyO(1)O(1) space via bit encoding
1041 Robot Boundedsimulate foreverone pass + a rotation argument
6 Zigzagbuild the full grid, O(nrows)O(n \cdot \text{rows})bucket per row, O(n)O(n)

The variant map

VariantThe techniqueCanonical problem
Cellular updateEncode next state in spare bits289
Robot with turnsDirection vectors or the rotation pair1041 · 874 · 657
Unusual output orderBucket by target row/column6 · 54 · 498
Formatting / justificationCareful integer division of spare space68 · 12 · 273
Enormous step countFind the cycle, then use modular arithmetic1041 · 957
Obstacle-aware movementA setset of obstacles for O(1)O(1) lookup874

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 O(mn8)=O(mn)O(m \cdot n \cdot 8) = O(mn). Space O(1)O(1).

[[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 O(mn)O(mn)-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 O(1)O(1) answer.

Follow-uups you should expect:

  • “What if the board is infinite?” LeetCode asks this directly. Store only the live cells as a setset of 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 O(n)O(n), one pass. Space O(1)O(1).

The test cases cover each branch:

  • "GG""GG" gives FalseFalse — no rotation, non-zero displacement; the only unbounded shape.
  • "GL""GL" gives TrueTrue — rotated, so bounded even though it moved.
  • "GGLLGG""GGLLGG" gives TrueTrue — returns to the origin exactly.
  • "GLGLGGLGL""GLGLGGLGL" gives FalseFalse — 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" gives TrueTrue — rotation with no movement at all.
  • "GLRLLG""GLRLLG" gives TrueTrue — 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 O(1)O(1) 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 O(n)O(n). Space O(n)O(n) for the output.

Three details:

  • numRows == 1numRows == 1 must be guarded. With one row, row == 0row == 0 and row == numRows - 1row == numRows - 1 are both true. The if row == 0if row == 0 branch wins, sets step = 1step = 1, and rowrow becomes 11 — out of range on the next append. Returning ss unchanged is both safe and correct.
  • Check row == 0row == 0 first. The ordering resolves the ambiguous single-row case deterministically, which is what makes the guard sufficient.
  • numRowsnumRows may exceed the string length. ("AB", 5)("AB", 5) gives "AB""AB": the walk never reaches the bottom, so stepstep stays 11, 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 O(n)O(n) 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 O(n)O(n) 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?” — O(nnumRows)O(n \cdot \text{numRows}) 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

#ProblemDifficultyThe twist
657Robot Return to OriginEasyJust count opposing moves; no simulation needed
6Zigzag ConversionMediumBouncing step; guard numRows == 1numRows == 1
289Game of LifeMediumTwo-bit encoding for simultaneous in-place update
1041Robot Bounded In CircleMediumOne pass + a rotation argument — do not simulate forever
874Walking Robot SimulationMediumGenuine simulation; obstacles in a setset for O(1)O(1) lookup
68Text JustificationHardPure specification-following; the last line is left-justified

Interview follow-ups

They askWhat they’re checkingThe answer
“Should you simulate at all?”The most important judgementNot when the step count is huge — find a cycle, a closed form, or an invariant
“How do you handle simultaneous updates?”The in-place trickEncode 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 argument90° rotations have order 4, so four passes compose to a closed loop
“What if the board were infinite?”Real-world scalingStore only live cells in a setset; work scales with the live population
“Why not build the 2D grid for zigzag?”Space awarenessO(nnumRows)O(n \cdot \text{numRows}) and mostly empty; buckets per row are O(n)O(n)
“How do you avoid off-by-ones here?”MethodEnumerate 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.
  • numRowsnumRows greater than len(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" gives TrueTrue, never moving.
  • Instructions with no turns"GG""GG" gives FalseFalse, the only unbounded shape.
  • Turns that cancel to zero net rotation"GLGLGGLGL""GLGLGGLGL" gives FalseFalse; 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-branch ifif chains. Python’s non-negative %% makes (i - 1) % 4(i - 1) % 4 safe.
  • For simultaneous updates, encode the next state in spare bits — read with & 1& 1, write with |= 2|= 2, then shift. Offer the O(mn)O(mn) copy first; the bit trick is the O(1)O(1) follow-up.
  • A bouncing step variable beats modular index arithmetic for zigzag traversal — and numRows == 1numRows == 1 must 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 coffee

Was this page helpful?

Let us know how we did