Algorithm

Open the Lock

Breadth-First Search (BFS) Pattern

Open the Lock

A lock has 4 circular wheels, each showing a digit from 0 to 9. One move turns a single wheel one slot in either direction, and the wheels wrap around, so 9 turns up to 0 and 0 turns down to 9.

The lock starts at "0000". You are given a list of deadends: if the display ever shows one of these codes the wheels jam and the lock can never be opened, so those codes may not be passed through even in the middle of a sequence.

Return the minimum number of moves needed to reach target, or -1 if it cannot be reached.

CONSTRAINTS
  • 1 <= deadends.length <= 500
  • deadends[i].length == 4, target.length == 4
  • All strings consist of digits only
  • target will not appear in deadends
  • target is not "0000"
  • "0000" itself may be a deadend, in which case the lock cannot even be started.
EXAMPLE 1
Input: deadends = ["0201","0101","0102","1212","2002"], target = "0202"
Output: 6
One shortest route is 0000, 1000, 1100, 1200, 1201, 1202, 0202 — six moves. The direct-looking route 0000, 0100, 0200, 0201, 0202 is only four moves but passes through 0201, which is a deadend, so it is not permitted. Blocked codes can force a longer detour.
EXAMPLE 2
Input: deadends = ["8888"], target = "0009"
Output: 1
Turning the last wheel down from 0 wraps it to 9 in a single move. The wrap-around is what makes this 1 move instead of the 9 upward turns it would otherwise take.
EXAMPLE 3
Input: deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"], target = "8888"
Output: -1
Those eight codes are exactly the eight neighbours of 8888, so every route into the target is jammed. The target itself is reachable in principle but has no usable approach.
EXAMPLE 4
Input: deadends = ["0000"], target = "8888"
Output: -1
The starting code is itself a deadend, so the wheels jam before any move is made. This must be checked before the search begins.
Do deadends only block the final code, or the whole route?
The whole route. Displaying a deadend at any point jams the lock, so a deadend can never appear as an intermediate step either. It is a removed node, not merely a forbidden goal.
Can "0000" be a deadend even though the lock starts there?
Yes, and then the answer is -1 immediately. It is a cheap check to make first, and forgetting it means the search starts from a state that should have jammed.
Does one move turn a single wheel or can I turn several at once?
A single wheel, by a single slot. So each code has exactly 8 successors: four wheels, each turnable up or down.
How large is the space of codes I might have to search?
Exactly 10,000, from 0000 to 9999. That number is worth computing out loud in an interview, because it tells you a full search is affordable and rules out any need for cleverness.

The statement is about a physical object, so start by translating it into something countable. A state is one complete display of four digits — not one wheel, but the whole code, since it is the whole code that gets compared against the deadend list. How many states exist? Four wheels with ten positions each gives 10 × 10 × 10 × 10 = 10,000.

That number decides the shape of the solution before any algorithm is chosen. Ten thousand states, each with 8 possible moves, is 80,000 transitions in total — trivial for a computer. So there is no need for a formula or a shortcut. Visiting every reachable state, once, is entirely affordable. The only requirement is to visit each one once.

Making that count is a habit worth building. In any puzzle about configurations, work out how many configurations exist. If the number is small, an exhaustive search is the answer and the interview is really about doing it without redundancy. If it explodes, you need pruning or a completely different idea.

The graph hiding in the lock

Recast the problem in the vocabulary of paths:

- A node is a 4-digit code.
- An edge joins two codes that differ by turning one wheel one slot — including the wrap between 9 and 0.
- Every edge costs one move.
- Deadends are simply nodes deleted from the graph. Nothing special, just absent.

Now "minimum number of moves" is literally "shortest path from 0000 to target", and every move costing the same is the property that makes the easy method valid.

Why does equal cost matter so much? Because it means you can expand outward in layers and trust the order of discovery. Layer 1 is all codes one move from the start, layer 2 is all unclaimed codes one move from those, and so on. A code first appears in the layer equal to its true distance — earlier is impossible, since that would require a shorter route which by definition does not exist, and later cannot happen, because the moment a layer-d code is expanded it claims all of its unclaimed neighbours for layer d+1. The first arrival at target is therefore the best arrival, and the search can stop right there.

This layered expansion is Breadth-First Search. It needs a queue, which returns codes in the order they were found; that FIFO order is what stops layers from mixing. Swap in a stack and the search dives down one long chain of turns before finishing the current layer, and the first time it stumbles onto target the move count is just some route, not the shortest.

The mechanism

Two details are specific to this problem: generating the 8 neighbours with wrap-around, and handling deadends.

Wrap-around is arithmetic. Adding 1 to a digit and taking the remainder on division by 10 turns 9 into 0. Subtracting 1 the same way turns 0 into 9, because in Python (0 - 1) % 10 is 9. Writing both directions as (d + delta) % 10 with delta of +1 and -1 removes every special case.

python
from collections import deque

def open_lock(deadends, target):
    dead = set(deadends)                  # set: constant-time membership
    if "0000" in dead:
        return -1
    if target == "0000":
        return 0

    queue = deque([("0000", 0)])
    visited = {"0000"}

    while queue:
        code, turns = queue.popleft()
        if code == target:
            return turns

        for i in range(4):                        # which wheel
            for delta in (1, -1):                 # which direction
                digit = (int(code[i]) + delta) % 10
                nxt = code[:i] + str(digit) + code[i + 1:]
                if nxt not in dead and nxt not in visited:
                    visited.add(nxt)              # claim on discovery
                    queue.append((nxt, turns + 1))
    return -1
Crucial Notea code is added to visited the instant it is generated, not when it is later removed from the queue. This is the classic BFS bug. A code like 1100 is reachable from 1000, 1200, 1110 and 1190 — if visiting is only recorded on removal, all four of those push their own copy before any of them is processed, and each copy then expands the same subtree. The queue fills with duplicates, memory grows, and the running time stops being linear in the number of states. Claiming on discovery guarantees each of the 10,000 codes enters the queue at most once.

Note also that deadends and visited codes are rejected at the same point, by the same kind of check. That is the graph view paying off: a jammed code and an already-explored code are both simply not somewhere to go, so they need no separate logic.

Each of at most 10,000 codes is expanded once, and each expansion builds 8 strings of 4 characters. The work is O(10⁴ × 8) with O(10⁴) memory — a fixed budget that does not grow with the input, since the deadend list only ever shrinks the space.

Worked example

Take deadends {8887, 8889, 8878, 8898, 8788, 8988, 7888, 9888} with target 8888.

The search starts at 0000 and spreads normally. Nothing near the origin is blocked, so layer 1 holds the eight codes 1000, 9000, 0100, 0900, 0010, 0090, 0001, 0009, layer 2 holds everything two turns out, and the wave keeps growing. Every code that differs from 8888 in at least one wheel by two or more slots gets claimed eventually.

But look at what happens at the boundary. To reach 8888 the last move must come from a code differing in exactly one wheel by one slot — and those are exactly 8887, 8889, 8878, 8898, 8788, 8988, 7888 and 9888. All eight are deadends, so all eight fail the nxt not in dead test and are never queued. The search therefore never generates 8888 at all. Eventually every reachable code is claimed, the queue drains, the loop exits, and the answer is -1.

Two things are worth noticing. The target was never touched even though it is not itself a deadend — reachability is about the approaches, not the destination. And the -1 falls out of the queue emptying naturally; no separate detection is needed.

Now the first example, with deadends {0201, 0101, 0102, 1212, 2002} and target 0202. The tempting short route is 0000, 0100, 0200, 0201, 0202 — four moves. But 0201 is jammed, so when 0200 generates it the code is rejected and the route dies there. Meanwhile 0100 is also a deadend, so even that first step is unavailable. BFS does not need to reason about any of this; it simply never queues those codes, and its expanding layers find the six-move route 0000, 1000, 1100, 1200, 1201, 1202, 0202 as soon as layer 6 is reached. The answer is 6.

Where this pattern shows up next

Strip away the wheels and this is the same shape as the word-transformation problem: a set of configurations, a rule generating the legal moves from any configuration, some configurations forbidden, and a question about the minimum number of moves. The differences are cosmetic — codes instead of words, wheel turns instead of letter swaps.

What distinguishes this one is that the state space is finite and countable in advance. You know before writing a line that there are 10,000 states. In the word problem, the space is bounded by the dictionary; in a puzzle like the sliding 8-puzzle it is 9 factorial, or 362,880; in a problem about arbitrary integers it may be unbounded, and then you need a cutoff or you never terminate. Sizing the space first tells you whether an exhaustive search is even an option.

The same doubling trick applies here as elsewhere. Searching outward from 0000 alone expands roughly 8^d codes by depth d. Searching simultaneously backward from target and expanding whichever frontier is smaller has both sides meet around depth d/2, cutting the number of codes examined dramatically. It is valid because turning a wheel up is undone by turning it down — the move relation is symmetric, so the graph can be walked in either direction.

Train the reflex: when a puzzle asks for the fewest moves between two configurations, count the configurations first. A small, countable space plus uniform move costs means BFS, and the only real work left is generating neighbours correctly and claiming each state exactly once.

Interactive Strategy Visualization
Phase 1 — Start
State-Space BFS: Lock Rotations
0
0
0
0
2 Deadends
1 in Queue
Search Queue (1)0000 (0)

Start: We begin at "0000" and want to reach "0202" by rotating one wheel at a time.

STEP 1/158
Exponential Explore Every Move Sequence
O(10⁴ × 8) BFS Over All Codes
Bidirectional BFS Meets Halfway