Open the Lock

Updated on 23 June, 2025
Open the Lock header image

Problem Statement

Imagine a security mechanism with a combination lock consisting of four circular wheels, each labeled with digits from '0' to '9'. These wheels can rotate freely in both directions, allowing each digit to wrap around from '9' back to '0', and vice versa. Each single move involves turning one wheel by one slot.

The challenge begins with the lock set to '0000', and you're tasked with manipulating it to reach a designated "unlock" combination, referred to as the target. However, there is a twist: certain combinations termed as deadends immediately disable the lock if reached, making further movement impossible.

Your objective is to determine the minimal number of moves required to transform the initial state '0000' to the target combination without hitting any deadends along the way. If reaching the target is unfeasible due to these restrictions, the function should return -1.

Examples

Example 1

Input:

deadends = ["0201","0101","0102","1212","2002"], target = "0202"

Output:

6

Explanation:

A sequence of valid moves would be "0000" -> "1000" -> "1100" -> "1200" -> "1201" -> "1202" -> "0202".
Note that a sequence like "0000" -> "0001" -> "0002" -> "0102" -> "0202" would be invalid,
because the wheels of the lock become stuck after the display becomes the dead end "0102".

Example 2

Input:

deadends = ["8888"], target = "0009"

Output:

1

Explanation:

We can turn the last wheel in reverse to move from "0000" -> "0009".

Example 3

Input:

deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"], target = "8888"

Output:

-1

Explanation:

We cannot reach the target without getting stuck.

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.

Approach and Intuition

The problem can be visualized as a shortest path search within an unweighted graph, where each node represents a possible state of the lock (i.e., a four-digit combination) and each edge corresponds to a single move or transition between states. The solution to navigating this space efficiently leans towards Breadth-First Search (BFS), a classic technique for finding shortest paths in such scenarios.

Key Steps in the Approach:

  1. Initial Setup: Begin by marking all deadends as visited to prevent their inclusion in the search path.
  2. Queue Initialization: Start the BFS with the queue initialized to the starting point '0000', provided it's not a deadend.
  3. State Exploration: For each state, explore all potential states that can be reached by turning any of the four wheels one slot to the left or right.
  4. Avoid Revisits: Maintain a set of visited states to avoid cycles or revisiting the same state, which would otherwise lead to inefficiencies and potential infinite loops.
  5. Check for Target: At each step, check whether the target state is reached. If so, return the number of moves made.
  6. Termination: If the queue gets exhausted without finding the target, return -1, indicating the target is unachievable from the given starting point without hitting a deadend.

Handling Edge Cases:

  • If '0000' itself is a deadend, immediate recognition of this scenario is essential to return -1 outright, as no moves can be made.
  • It's critical to efficiently manage transitions between states, ensuring that each possible move adheres to the wrap-around behavior of the wheels.

By adhering to this structured approach, the solution not only achieves the goal of finding the minimum number of moves (or determining impossibility) but does so in an optimal manner respecting the problem's constraints and conditions.

Solutions

  • C++
  • Java
  • Python
cpp
class Solution {
public:
    int unlockSafe(vector<string>& blocklist, string goal) {
        unordered_map<char, char> nextDigit = {
            {'0', '1'}, 
            {'1', '2'}, 
            {'2', '3'}, 
            {'3', '4'}, 
            {'4', '5'},
            {'5', '6'}, 
            {'6', '7'}, 
            {'7', '8'}, 
            {'8', '9'}, 
            {'9', '0'}
        };
        unordered_map<char, char> prevDigit = {
            {'0', '9'}, 
            {'1', '0'}, 
            {'2', '1'}, 
            {'3', '2'}, 
            {'4', '3'},
            {'5', '4'}, 
            {'6', '5'}, 
            {'7', '6'}, 
            {'8', '7'}, 
            {'9', '8'}
        };

        unordered_set<string> blockedConfigs(blocklist.begin(), blocklist.end());
        queue<string> dropsQueue;

        int minSteps = 0;

        if (blockedConfigs.find("0000") != blockedConfigs.end()) {
            return -1;
        }

        dropsQueue.push("0000");
        blockedConfigs.insert("0000");

        while (!dropsQueue.empty()) {
            for (int size = dropsQueue.size(); size > 0; --size) {
                string currentConfig = dropsQueue.front();
                dropsQueue.pop();

                if (currentConfig == goal) {
                    return minSteps;
                }

                for (int wheel = 0; wheel < 4; ++wheel) {
                    string newConfig = currentConfig;
                    newConfig[wheel] = nextDigit[newConfig[wheel]];
                    if (blockedConfigs.find(newConfig) == blockedConfigs.end()) {
                        dropsQueue.push(newConfig);
                        blockedConfigs.insert(newConfig);
                    }

                    newConfig = currentConfig;
                    newConfig[wheel] = prevDigit[newConfig[wheel]];
                    if (blockedConfigs.find(newConfig) == blockedConfigs.end()) {
                        dropsQueue.push(newConfig);
                        blockedConfigs.insert(newConfig);
                    }
                }
            }
            minSteps++;
        }
        return -1;
    }
};

This C++ program aims to solve the "Open the Lock" problem using a breadth-first search (BFS) approach. The function unlockSafe uses data structures and algorithms to determine the minimum number of turns required to open a lock. Here's a detailed breakdown of the solution:

  • The nextDigit and prevDigit are unordered maps mapping each digit to their respective next and previous digits on a lock.
  • A set blockedConfigs initialized with a list of blocked lock configurations prevents visiting configurations that are unsolvable.
  • A queue dropsQueue initializes with the lock's start configuration "0000" and performs BFS to explore all feasible configurations.
  • The solution prevents progression into any blocked configuration by checking blockedConfigs before enqueuing new configurations derived by turning the wheels to the next or previous digits.
  • If the BFS finds the goal configuration, the function returns the count of moves required; if the queue exhausts without finding the solution, it returns -1, indicating the goal is unachievable with the given blocked states.

This code efficiently handles the exploration of possible states and ensures the shortest path to solve the lock is found, bounded by the constraints of blocked configurations.

java
class Solution {
    public int unlockSafe(String[] restricted, String goal) {
        Map<Character, Character> nextMapping = Map.of(
            '0', '1', 
            '1', '2', 
            '2', '3', 
            '3', '4', 
            '4', '5', 
            '5', '6', 
            '6', '7', 
            '7', '8', 
            '8', '9', 
            '9', '0'
        );
        Map<Character, Character> prevMapping = Map.of(
            '0', '9', 
            '1', '0', 
            '2', '1', 
            '3', '2', 
            '4', '3', 
            '5', '4', 
            '6', '5', 
            '7', '6', 
            '8', '7', 
            '9', '8'
        );

        Set<String> seenCombinations = new HashSet<>(Arrays.asList(restricted));
        Queue<String> combinationsQueue = new LinkedList<String>();

        int turnsCount = 0;

        if (seenCombinations.contains("0000")) {
            return -1;
        }

        combinationsQueue.add("0000");
        seenCombinations.add("0000");

        while (!combinationsQueue.isEmpty()) {
            int nodesInCurrentLevel = combinationsQueue.size();
            for (int idx = 0; idx < nodesInCurrentLevel; idx++) {
                String current = combinationsQueue.poll();

                if (current.equals(goal)) {
                    return turnsCount;
                }

                for (int slot = 0; slot < 4; slot++) {
                    StringBuilder newCombNext = new StringBuilder(current);
                    newCombNext.setCharAt(slot, nextMapping.get(newCombNext.charAt(slot)));
                    if (!seenCombinations.contains(newCombNext.toString())) {
                        combinationsQueue.add(newCombNext.toString());
                        seenCombinations.add(newCombNext.toString());
                    }

                    StringBuilder newCombPrev = new StringBuilder(current);
                    newCombPrev.setCharAt(slot, prevMapping.get(newCombPrev.charAt(slot)));
                    if (!seenCombinations.contains(newCombPrev.toString())) {
                        combinationsQueue.add(newCombPrev.toString());
                        seenCombinations.add(newCombPrev.toString());
                    }
                }
            }
            turnsCount++;
        }
        return -1;
    }
}

In the Java-based solution for the "Open the Lock" problem, you employ the Breadth-First Search (BFS) algorithm to find the minimum number of turns required to unlock a safe whose combinations change via a simple button press that causes each dial on the lock to either increase or decrease numeral value. Here's a concise breakdown of how the code operates:

  1. Define nextMapping and prevMapping to determine what each digit transitions to when incremented or decremented, respectively. These mappings provide quick access to adjacent lock states.

  2. Utilize a HashSet called seenCombinations to keep track of combinations that are either restricted or already processed in order to prevent reprocessing the same combination multiple times and to verify if the initial "0000" state is restricted.

  3. Use a Queue named combinationsQueue to manage the lock combinations yet to be evaluated in the BFS process.

  4. Initialize the BFS with the "0000" start combination if it's not restricted and begin processing from there. Each level of BFS represents one turn of the lock. For each combination processed per turn:

    • Check if it matches the goal combination to early return the current number of turns taken.

    • Generate the next and previous combinations by altering each digit of the lock, one at a time, according to nextMapping and prevMapping.

    • If these generated combinations are not already seen (and hence not restricted), they are enqueued for subsequent processing and marked as seen.

  5. If the goal combination is reached during processing, the function returns the count of turns taken. If no solution is found, the function returns -1.

This approach ensures that you find the efficient number of steps to reach a specified combination while avoiding restricted codes efficiently using mappings and BFS.

python
class Solution:
    def unlockSafe(self, blocklist: List[str], goal: str) -> int:
        forward_mapping = {
            "0": "1",
            "1": "2",
            "2": "3",
            "3": "4",
            "4": "5",
            "5": "6",
            "6": "7",
            "7": "8",
            "8": "9",
            "9": "0",
        }
        backward_mapping = {
            "0": "9",
            "1": "0",
            "2": "1",
            "3": "2",
            "4": "3",
            "5": "4",
            "6": "5",
            "7": "6",
            "8": "7",
            "9": "8",
        }

        visited = set(blocklist)
        queue = deque()
        step_count = 0

        if "0000" in visited:
            return -1

        queue.append("0000")
        visited.add("0000")

        while queue:
            level_size = len(queue)
            for _ in range(level_size):
                current = queue.popleft()
 
                if current == goal:
                    return step_count

                for i in range(4):
                    next_config = list(current)
                    next_config[i] = forward_mapping[next_config[i]]
                    next_config_str = "".join(next_config)
                    if next_config_str not in visited:
                        queue.append(next_config_str)
                        visited.add(next_config_str)

                    prev_config = list(current)
                    prev_config[i] = backward_mapping[prev_config[i]]
                    prev_config_str = "".join(prev_config)
                    if prev_config_str not in visited:
                        queue.append(prev_config_str)
                        visited.add(prev_config_str)

            step_count += 1

        return -1

The Python program provided aims to solve the problem of opening a lock by finding the shortest sequence of turns from a starting position of "0000" to a target combination, referred to as the goal. The process needs to avoid combinations listed in the blocklist. Here's how the solution operates:

  • Dictionary for Navigation: Utilizes two dictionaries to determine the next possible numbers for every wheel in the lock, both forward and backward.

  • Handling Blocked Combinations: Start by marking "0000" as visited if it's not on the blocklist. If "0000" is blocked, the function returns -1 as it's impossible to start.

  • Breadth-First Search Approach: Implementing a queue to explore each combination layer by layer. Every level of the queue represents a move across all four wheels of the lock. For each position:

    • Convert the combination to the next and previous viable number per wheel using the mappings.
    • Check if these new combinations have been visited or are in the blocklist.
    • If a viable configuration is found that matches the goal, the current count of steps is returned.
  • Tracking Steps: Count each layer-level processed as one step, incrementing until the goal is found or all possibilities are exhausted.

This solution ensures an efficient exploration of combinations using a systematic breadth-first search, preventing revisits and skips blocked states. If the goal is unachievable, it correctly returns -1 indicating failure. With each possible configuration tried systematically, the solution guarantees the shortest path to unlocking the lock, if it exists.

Comments

No comments yet.