Open the Lock

Medium BFS

Problem

Given a list of deadend combinations and a target combination, return the minimum number of moves required to reach the target from '0000' by turning one wheel one digit at a time, avoiding deadends; return -1 if impossible.

  • 1 ≤ deadends.length ≤ 500
  • deadends[i].length == 4
  • target.length == 4
  • target will not be in deadends
  • Each string consists of digits from '0' to '9'

Example

Input: deadends = ["0201","0101","0102","1212","2002"], target = "0202"
Output: 6

Starting from '0000', the BFS explores all valid states layer by layer. It avoids deadends by skipping them. The critical moment occurs when the algorithm reaches '0202' after 6 moves. The BFS guarantees this is the shortest path because it explores all states reachable in fewer moves first. The queue maintains states paired with their move counts, ensuring the first time the target is dequeued corresponds to the minimal moves.

Approach

Straightforward Solution

A brute-force approach might try all possible sequences of moves without pruning, leading to exponential time and repeated states. This is infeasible due to the large state space (10,000 possible states).

Core Observation

The lock's state space can be modeled as a graph where each node is a 4-digit string and edges connect states differing by one wheel turn. The shortest path in this unweighted graph corresponds to the minimum moves to reach the target.

Path to Optimal

Recognizing the problem as shortest path in an unweighted graph suggests BFS. BFS explores states in order of increasing distance from the start, guaranteeing the first time the target is found is the shortest path. Using a queue and a visited set prevents revisiting states and infinite loops. Deadends are treated as blocked nodes and skipped during exploration.

Optimal Approach

Use BFS starting from '0000'. Maintain a queue of (state, moves) pairs and a visited set. For each dequeued state, generate all neighbors by incrementing or decrementing each digit. Skip neighbors in deadends or already visited. When the target is dequeued, return the associated move count. If the queue empties without finding the target, return -1.

Time

O(10^4)

The state space consists of 10,000 possible lock combinations (0000 to 9999). Each state generates up to 8 neighbors (4 wheels × 2 directions). BFS visits each state at most once, so the total operations are bounded by O(10^4).

Space

O(10^4)

The visited set and queue can hold up to all possible states in the worst case, resulting in O(10^4) auxiliary space. This is necessary to avoid revisiting states and infinite loops.

Pattern Spotlight

BFS (Shortest Path on Unweighted Graph)

When a problem asks for the minimum number of moves or steps in a state space where each move has equal cost, model the problem as a graph and use BFS to explore states layer by layer, ensuring the first time the target is reached corresponds to the shortest path.

Solution

1class Solution:
2 def openLock(self, deadends: List[str], target: str) -> int:
3 dead = set(deadends)
4
5 if "0000" in dead:
6 return -1
7
8 q = deque([("0000", 0)])
9 seen = {"0000"}
10
11 while q:
12 state, moves = q.popleft()
13
14 if state == target:
15 return moves
16
17 for i in range(4):
18 digit = int(state[i])
19
20 for step in (-1, 1):
21 new_digit = (digit + step) % 10
22 next_state = state[:i] + str(new_digit) + state[i+1:]
23
24 if next_state not in dead and next_state not in seen:
25 seen.add(next_state)
26 q.append((next_state, moves + 1))
27
28 return -1

Step-by-Step Solution

1

Convert Deadends to a Set for O(1) Lookup

3dead = set(deadends)

Objective

To efficiently check if a state is a deadend by storing deadends in a hash set.

Key Insight

Deadends represent forbidden states that must be avoided. Using a set allows constant-time membership checks, which is critical for pruning the BFS search space efficiently. Without this, checking deadends would be O(n) per state, drastically increasing runtime.

Interview Quick-Check

Core Logic

Using a set for deadends enables O(1) membership checks, which is essential for pruning invalid states during BFS.

Common Pitfalls & Bugs

Failing to convert deadends to a set leads to inefficient lookups and potential timeouts.

2

Handle Immediate Deadend Starting State

5if "0000" in dead:
6 return -1

Objective

To immediately return -1 if the initial state '0000' is a deadend, as no moves are possible.

Key Insight

If the starting state is blocked, no exploration is possible. Detecting this early prevents unnecessary computation and correctly returns the failure case.

Interview Quick-Check

State & Boundaries

Check if '0000' is in deadends before starting BFS to handle trivial no-solution cases.

3

Initialize BFS Queue and Visited Set with Starting State

8q = deque([("0000", 0)])
9seen = {"0000"}

Objective

To set up the BFS traversal starting from '0000' with zero moves made.

Key Insight

The queue stores states paired with the number of moves taken to reach them, enabling BFS to track distance from the start. The visited set prevents revisiting states, avoiding infinite loops and redundant work.

Interview Quick-Check

Core Logic

The queue holds tuples of (state, moves) to track BFS layers and distances.

State & Boundaries

The visited set ensures each state is processed only once, maintaining BFS correctness and efficiency.

4

Perform BFS to Explore All Valid Next States

11while q:
12 state, moves = q.popleft()
14 if state == target:
15 return moves
17 for i in range(4):
18 digit = int(state[i])
20 for step in (-1, 1):
21 new_digit = (digit + step) % 10
22 next_state = state[:i] + str(new_digit) + state[i+1:]
24 if next_state not in dead and next_state not in seen:
25 seen.add(next_state)
26 q.append((next_state, moves + 1))

Objective

To iteratively dequeue states, check for the target, and enqueue all valid neighbors not in deadends or visited.

Key Insight

BFS explores the state space layer by layer. For each state, generating neighbors by incrementing or decrementing each wheel digit covers all possible one-move transitions. Skipping deadends and visited states prunes the search space. Returning immediately upon reaching the target guarantees minimal moves.

Interview Quick-Check

Core Logic

BFS explores states in order of increasing moves, generating neighbors by changing each digit up or down with wrap-around.

State & Boundaries

The loop continues until the queue is empty or the target is found, ensuring exhaustive search of reachable states.

Common Pitfalls & Bugs

Forgetting to mark neighbors as visited before enqueueing can cause duplicate processing and inefficiency.

Complexity

Each state is enqueued at most once, bounding the BFS runtime to the size of the state space.

5

Return -1 if Target is Unreachable

28return -1

Objective

To return -1 when BFS exhausts all reachable states without finding the target.

Key Insight

If the queue empties without dequeuing the target, it means the target is unreachable due to deadends or disconnected state space. Returning -1 signals failure to the caller.

Interview Quick-Check

State & Boundaries

Return -1 only after all reachable states have been explored and the target was not found.

Line Analysis

This solution has 4 Critical lines interviewers watch for.

Line 14 Critical
if state == target:

Check if the current state matches the target.

Finding the target means the shortest path has been found, so the algorithm can return immediately.

Line 5 Critical
if "0000" in dead:

Check if the initial state '0000' is a deadend.

If the start state is blocked, no moves are possible, so the function must return -1 immediately to avoid unnecessary computation.

Line 15 Critical
return moves

Return the number of moves taken to reach the target.

This terminates the search with the minimal move count, guaranteed by BFS ordering.

Line 24 Critical
if next_state not in dead and next_state not in seen:

Check if the new state is neither a deadend nor visited.

Pruning deadends and already visited states prevents invalid or redundant exploration, ensuring BFS efficiency and correctness.

Line 3 Core
dead = set(deadends)

Convert deadends list into a set for fast membership checking.

Using a set enables O(1) average-time lookups to quickly determine if a state is forbidden, which is critical for pruning during BFS.

Line 8 Core
q = deque([("0000", 0)])

Initialize the BFS queue with the starting state and zero moves.

The queue stores states paired with their distance from the start, enabling BFS to explore states in order of increasing moves.

Line 9 Core
seen = {"0000"}

Initialize the visited set with the starting state.

Tracking visited states prevents revisiting and infinite loops, ensuring BFS explores each state only once.

Line 12 Core
state, moves = q.popleft()

Dequeue the next state and its move count.

FIFO ordering ensures states are explored in order of increasing moves, which is essential for BFS correctness.

Line 25 Core
seen.add(next_state)

Mark the new state as visited.

Adding to visited before enqueueing prevents duplicate entries and infinite loops.

Line 26 Core
q.append((next_state, moves + 1))

Enqueue the new state with incremented move count.

Scheduling the neighbor for future exploration maintains BFS layer-by-layer traversal and tracks distance.

Line 6 Core
return -1

Return -1 when the start state is a deadend.

This early return handles an edge case that prevents BFS from running on an impossible problem.

Line 11 Core
while q:

Begin BFS loop while there are states to explore.

The loop processes states layer by layer, guaranteeing shortest path discovery in an unweighted graph.

Line 21 Core
new_digit = (digit + step) % 10

Calculate the new digit after turning the wheel with wrap-around.

Using modulo 10 ensures digits wrap from 9 to 0 and vice versa, maintaining valid lock states.

Line 22 Core
next_state = state[:i] + str(new_digit) + state[i+1:]

Construct the new lock state string after the wheel turn.

Creating the new state string enables BFS to track and explore this neighbor state.

Line 28 Core
return -1

Return -1 if the target is unreachable after exploring all states.

Exhausting the queue without finding the target means no valid path exists, so returning -1 correctly signals failure.

Line 17 Standard
for i in range(4):

Iterate over each wheel position in the lock state.

Each wheel can be turned independently, so exploring all positions is necessary to generate all possible next states.

Line 20 Standard
for step in (-1, 1):

Iterate over the two possible wheel turns: decrement and increment.

Each wheel can be turned one step forward or backward, generating two neighbors per wheel position.

Line 18 Standard
digit = int(state[i])

Extract the digit at the current wheel position.

Knowing the current digit allows calculation of the next possible digits by incrementing or decrementing.

Test Your Understanding

Why does BFS guarantee that the first time the target state is dequeued corresponds to the minimum number of moves?

Because BFS explores states in order of increasing distance from the start, it visits all states reachable in k moves before any states reachable in k+1 moves. Therefore, the first time the target is dequeued, it must be via the shortest path.

Related Problems

BFS pattern

Don't just read it. Drill it.

Reconstruct Open the Lock from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.

Unlock the Open the Lock drill

or drill a free problem