Open the Lock
Open the Lock: A 4-wheel lock starts at 0000; find the minimum number of turns to reach target, avoiding any state in deadends.
- 1 <= deadends.length <= 500
- deadends[i].length == 4
- target.length == 4
- target will not be in the list deadends.
- target and deadends[i] consist of digits only.
Intuition
Open the lock turns a four-wheel combination lock from "0000" to a target, avoiding a set of deadends, using the fewest single-wheel turns. The word fewest is the signal.
Each lock state is a node, and turning one wheel one notch is an edge to another state. All turns cost the same, so this is a shortest path on an unweighted graph, which is BFS:
- Every state has exactly eight neighbours — four wheels, each turned up or down one notch.
The state space is bounded: 10⁴ = 10,000 combinations, small enough to explore exhaustively.
Wheels wrap, so the digit after 9 is 0 and before 0 is 9. Modular arithmetic handles both — (d + 1) % 10 and (d + 9) % 10. Forgetting the wrap silently removes valid moves and can make a reachable target look unreachable.
Deadends are simply states that may not be entered. The cleanest handling is to load them into the visited set before the search starts, so the traversal treats them as already-seen and never expands them. Checking them separately inside the loop works too but adds a branch on every neighbour.
One case is easy to miss: if "0000" is itself a deadend, the answer is −1 even when the target is "0000". Seeding the visited set first handles this automatically, since the start is then never enqueued.
Bidirectional BFS — searching from both start and target — cuts the explored frontier substantially and is worth knowing, though plain BFS is fast enough at this size.
BFS over lock states, with deadends folded into the visited set at initialisation. That single trick means the expansion loop needs no separate deadend test — an unreachable state and an already-seen state are handled identically.
Approach
Before reading on: price up what the direct approach costs here, then ask what one visit per cell or vertex would have to mark so nothing is counted twice. Aim for O(10^4) time and O(10^4) space.
Model states as graph nodes
Each four-digit combination is a node; a single wheel turn is an edge. Uniform move cost plus a shortest-path question means BFS, and the space is only 10,000 states.
Generate eight neighbours
For each of the four wheels, produce the state with that digit turned up and turned down. Every state has exactly eight successors, regardless of position.
Handle the wrap with modular arithmetic
Use (d + 1) % 10 and (d + 9) % 10 so 9 wraps to 0 and back. Forgetting the wrap removes valid moves and can make a reachable target appear unreachable.
Seed deadends into visited
Load every deadend into the visited set before the search begins. The traversal then skips them as already-seen, with no extra check on each neighbour.
Cover the blocked-start case
If "0000" is itself a deadend the answer is -1, even for target "0000". Seeding visited first handles this automatically — the start is never enqueued.
Expand level by level
Process the queue one level at a time, incrementing the turn count per level. The first time the target is dequeued, its level is the minimum number of turns.
Cost of the search
Each of the 10,000 states is visited once with eight neighbours generated, giving O(10^4 · 8) time — effectively constant for a fixed lock. Bidirectional BFS shrinks the frontier further.
Solution & live demo
Common pitfalls
Not wrapping the digits
nd = digit + delta
nd = (digit + delta) % 10
The wheels are circular: 9 turns to 0 and 0 turns to 9. Without the modulo the digit goes out of range and the state string becomes invalid or throws.
Checking deadends separately in the loop
if cand not in visited and cand not in dead:
visited = {'0000'} | deadTwo conditions to keep in sync at every expansion. Seeding the visited set with the deadends makes them unreachable by construction, with one lookup instead of two.
Missing the case where the start is a deadend
q = deque([('0000', 0)])if '0000' in dead:
return -1If the initial state is blocked the lock can never be turned, but the BFS would still expand from it since it was enqueued before any check. The guard has to come before the seed.
Edge cases
Return -1 before any traversal.
Zero turns needed, found immediately.
BFS queue empties without reaching target; return -1.
Turning down from 0 must land on 9, not -1 or an invalid digit.