752. Open The Lock - Explanation

Problem Link

Description

You have a lock with 4 circular wheels. Each wheel has 10 slots: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'. The wheels can rotate freely and wrap around: for example we can turn '9' to be '0', or '0' to be '9'. Each move consists of turning one wheel one slot.

The lock initially starts at '0000', a string representing the state of the 4 wheels.

You are given a list of deadends dead ends, meaning if the lock displays any of these codes, the wheels of the lock will stop turning and you will be unable to open it.

Given a target representing the value of the wheels that will unlock the lock, return the minimum total number of turns required to open the lock, or -1 if it is impossible.

Example 1:

Input: deadends = ["1111","0120","2020","3333"], target = "5555"

Output: 20

Example 2:

Input: deadends = ["4443","4445","4434","4454","4344","4544","3444","5444"], target = "4444"

Output: -1

Constraints:

  • 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.


Topics

Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Breadth-First Search (BFS) - Level-by-level graph traversal to find shortest paths in unweighted graphs
  • Graph Modeling - Recognizing when a problem can be modeled as a graph where states are nodes and transitions are edges
  • HashSet for Visited States - Efficiently tracking visited states to avoid revisiting and infinite loops
  • String Manipulation - Modifying individual characters in strings to generate neighbor states

1. Breadth First Search - I

Intuition

Think of each lock combination as a node in a graph, where two nodes are connected if you can reach one from the other by turning a single wheel one step. Starting from "0000", we want to find the shortest path to the target while avoiding deadends.

BFS is the natural choice here because it explores all states at distance 1 before distance 2, and so on. This guarantees that when we first reach the target, we've found the minimum number of moves. We treat deadends as blocked nodes and use a visited set to avoid revisiting the same combination.

Algorithm

  1. If "0000" is a deadend, return -1 immediately since we cannot even start.
  2. Initialize a queue with the starting state "0000" and 0 turns.
  3. Add all deadends to a visited set to block them.
  4. While the queue is not empty:
    • Dequeue a lock combination and its turn count.
    • If it matches the target, return the turn count.
    • Generate all 8 neighbors (each wheel can go up or down by 1).
    • For each unvisited neighbor, mark it visited and enqueue it with turns + 1.
  5. If the queue empties without finding the target, return -1.
class Solution:
    def openLock(self, deadends: List[str], target: str) -> int:
        if "0000" in deadends:
            return -1

        def children(lock):
            res = []
            for i in range(4):
                digit = str((int(lock[i]) + 1) % 10)
                res.append(lock[:i] + digit + lock[i+1:])
                digit = str((int(lock[i]) - 1 + 10) % 10)
                res.append(lock[:i] + digit + lock[i+1:])
            return res

        q = deque([("0000", 0)])
        visit = set(deadends)

        while q:
            lock, turns = q.popleft()
            if lock == target:
                return turns
            for child in children(lock):
                if child not in visit:
                    visit.add(child)
                    q.append((child, turns + 1))
        return -1

Time & Space Complexity

  • Time complexity: O(dn+m)O(d ^ n + m)
  • Space complexity: O(dn)O(d ^ n)

Where dd is the number of digits (09)(0 - 9), nn is the number of wheels (4)(4), and mm is the number of deadends.


2. Breadth First Search - II

Intuition

This is a cleaner implementation of the same BFS approach. Instead of generating all children at once through a helper function, we iterate through each of the 4 wheels and try both directions (+1 and -1) inline. The core idea remains the same: explore level by level to find the shortest path.

By processing all nodes at the current level before incrementing the step counter, we ensure the first time we reach the target corresponds to the minimum number of moves.

Algorithm

  1. Handle edge cases: if target is "0000", return 0. If "0000" is a deadend, return -1.
  2. Initialize a queue with "0000" and mark it visited.
  3. Set steps to 0 and process level by level:
    • Increment steps at the start of each level.
    • For each lock in the current level, try all 8 possible moves.
    • If a move reaches the target, return steps.
    • Otherwise, add unvisited states to the queue.
  4. Return -1 if the target is unreachable.
class Solution:
    def openLock(self, deadends: List[str], target: str) -> int:
        if target == "0000":
            return 0

        visit = set(deadends)
        if "0000" in visit:
            return -1

        q = deque(["0000"])
        visit.add("0000")
        steps = 0

        while q:
            steps += 1
            for _ in range(len(q)):
                lock = q.popleft()
                for i in range(4):
                    for j in [1, -1]:
                        digit = str((int(lock[i]) + j + 10) % 10)
                        nextLock = lock[:i] + digit + lock[i+1:]
                        if nextLock in visit:
                            continue
                        if nextLock == target:
                            return steps
                        q.append(nextLock)
                        visit.add(nextLock)
        return -1

Time & Space Complexity

  • Time complexity: O(dn+m)O(d ^ n + m)
  • Space complexity: O(dn)O(d ^ n)

Where dd is the number of digits (09)(0 - 9), nn is the number of wheels (4)(4), and mm is the number of deadends.


Intuition

Standard BFS explores outward from the start, which can lead to exploring many states before reaching a distant target. Bidirectional BFS improves this by searching from both ends simultaneously: one frontier starts at "0000" and another at the target. When they meet, we've found the shortest path.

The key optimization is to always expand the smaller frontier. This balances the search and reduces the total number of states explored, especially when the search space branches heavily in one direction.

Algorithm

  1. Handle edge cases for target "0000" and "0000" being a deadend.
  2. Create two sets: begin starting from "0000" and end starting from target.
  3. While both sets are non-empty:
    • If begin is larger than end, swap them to always expand the smaller one.
    • Increment steps and create a temporary set for the next level.
    • For each state in begin, generate all 8 neighbors.
    • If a neighbor is in end, the frontiers meet, so return steps.
    • Otherwise, if unvisited, add to the temp set and mark visited.
    • Replace begin with the temp set.
  4. Return -1 if the sets never meet.
class Solution:
    def openLock(self, deadends: List[str], target: str) -> int:
        if target == "0000":
            return 0

        visit = set(deadends)
        if "0000" in visit:
            return -1

        begin = {"0000"}
        end = {target}
        steps = 0

        while begin and end:
            if len(begin) > len(end):
                begin, end = end, begin

            steps += 1
            temp = set()
            for lock in begin:
                for i in range(4):
                    for j in [-1, 1]:
                        digit = str((int(lock[i]) + j + 10) % 10)
                        nextLock = lock[:i] + digit + lock[i+1:]

                        if nextLock in end:
                            return steps
                        if nextLock in visit:
                            continue
                        visit.add(nextLock)
                        temp.add(nextLock)
            begin = temp
        return -1

Time & Space Complexity

  • Time complexity: O(dn+m)O(d ^ n + m)
  • Space complexity: O(dn)O(d ^ n)

Where dd is the number of digits (09)(0 - 9), nn is the number of wheels (4)(4), and mm is the number of deadends.


Common Pitfalls

Not Checking if Starting Position is a Deadend

The initial state "0000" might itself be a deadend. Failing to check this before starting BFS leads to incorrect exploration from an invalid starting point. Always verify "0000" is not in the deadends set before beginning the search.

Incorrect Wheel Wraparound Logic

Turning a wheel from 9 up should go to 0, and turning from 0 down should go to 9. Using simple addition/subtraction without modular arithmetic causes invalid states like -1 or 10. Apply (digit + move + 10) % 10 to handle wraparound correctly.

Adding to Visited Set After Dequeuing Instead of Before Enqueuing

Marking nodes as visited when dequeuing rather than when enqueuing leads to the same state being added to the queue multiple times. This causes redundant processing and can significantly slow down the solution. Always mark a state as visited immediately when adding it to the queue.