Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

752. Open the Lock - Leetcode Solution

Problem Description

You are given a lock represented by a 4-wheel combination, where each wheel shows a digit from '0' to '9'. Each wheel can be rotated up or down by one digit in a single move (with wraparound: '9' goes to '0' and '0' goes to '9').

The lock starts at the combination "0000". You are given:

  • deadends: a list of combinations that must never be entered (if you reach one, you are stuck).
  • target: a string representing the lock's target combination.

Your task is to determine the minimum number of moves required to reach target from "0000", without entering any deadend combination. If it is impossible, return -1.

Constraints:

  • Each move can only change one wheel by one digit up or down.
  • You cannot revisit combinations (no cycles).
  • There may be multiple deadends or none at all.
  • There is always at most one valid solution path.

Thought Process

At first glance, you might consider trying all possible sequences of moves to reach the target. However, with 4 wheels and each wheel having 10 positions, there are 10,000 possible combinations. Brute-force search would be highly inefficient, especially with deadends.

The problem is similar to finding the shortest path in a graph, where each node is a lock combination, and edges connect combinations that differ by one wheel move. Since every move has equal cost, the Breadth-First Search (BFS) algorithm is a natural fit—it explores all combinations at each step, ensuring the shortest path is found.

To avoid revisiting combinations and getting stuck in cycles, we'll need a way to track visited combinations. Deadends act as impassable nodes, so we must avoid those as well.

Solution Approach

Let's break down the solution step by step:

  1. Model the Problem as a Graph:
    • Each 4-digit string is a node.
    • Each move (rotating a wheel up or down) is an edge to a neighboring node.
  2. Use BFS for Shortest Path:
    • Start from "0000".
    • At each step, generate all possible combinations by moving each wheel up or down.
    • Skip deadends and already visited combinations.
    • Keep track of the number of moves (levels in BFS).
  3. Efficient Data Structures:
    • Use a queue to manage BFS traversal.
    • Use a set for deadends for O(1) lookup.
    • Use another set to keep track of visited combinations.
  4. Edge Cases:
    • If "0000" is a deadend, return -1 immediately.
    • If target is "0000", return 0 moves.
  5. Stop When Target is Found:
    • When we dequeue the target combination, return the number of moves taken to reach it.

This approach ensures we always find the shortest path, or determine it's impossible.

Example Walkthrough

Let's walk through an example:

Input:

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

Step-by-step:

  1. Start at "0000". It's not a deadend.
  2. Move 1: Generate all neighbors of "0000" (e.g., "1000", "9000", "0100", etc.). Add valid ones to the queue.
  3. Move 2: For each combination in the queue, generate their neighbors, skipping deadends and visited.
  4. Continue this process, level by level, until "0202" is reached.
  5. The minimum number of moves required in this example is 6.

The BFS ensures that as soon as we reach "0202", we've found the shortest possible path.

Code Implementation

from collections import deque

class Solution:
    def openLock(self, deadends, target):
        dead = set(deadends)
        visited = set('0000')
        if '0000' in dead:
            return -1
        queue = deque([('0000', 0)])
        while queue:
            node, depth = queue.popleft()
            if node == target:
                return depth
            for i in range(4):
                digit = int(node[i])
                for move in (-1, 1):
                    new_digit = (digit + move) % 10
                    neighbor = node[:i] + str(new_digit) + node[i+1:]
                    if neighbor not in visited and neighbor not in dead:
                        visited.add(neighbor)
                        queue.append((neighbor, depth + 1))
        return -1
      
#include <string>
#include <vector>
#include <queue>
#include <unordered_set>
using namespace std;

class Solution {
public:
    int openLock(vector<string>& deadends, string target) {
        unordered_set<string> dead(deadends.begin(), deadends.end());
        unordered_set<string> visited;
        if (dead.count("0000")) return -1;
        queue<pair<string, int>> q;
        q.push({"0000", 0});
        visited.insert("0000");
        while (!q.empty()) {
            auto [node, depth] = q.front(); q.pop();
            if (node == target) return depth;
            for (int i = 0; i < 4; ++i) {
                char c = node[i];
                for (int move : {-1, 1}) {
                    string neighbor = node;
                    neighbor[i] = (c - '0' + move + 10) % 10 + '0';
                    if (!dead.count(neighbor) && !visited.count(neighbor)) {
                        visited.insert(neighbor);
                        q.push({neighbor, depth + 1});
                    }
                }
            }
        }
        return -1;
    }
};
      
import java.util.*;

class Solution {
    public int openLock(String[] deadends, String target) {
        Set<String> dead = new HashSet<>(Arrays.asList(deadends));
        Set<String> visited = new HashSet<>();
        if (dead.contains("0000")) return -1;
        Queue<String> queue = new LinkedList<>();
        queue.offer("0000");
        visited.add("0000");
        int depth = 0;
        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int k = 0; k < size; ++k) {
                String node = queue.poll();
                if (node.equals(target)) return depth;
                for (int i = 0; i < 4; ++i) {
                    char[] chars = node.toCharArray();
                    for (int move : new int[]{-1, 1}) {
                        chars[i] = (char)((chars[i] - '0' + move + 10) % 10 + '0');
                        String neighbor = new String(chars);
                        if (!dead.contains(neighbor) && !visited.contains(neighbor)) {
                            visited.add(neighbor);
                            queue.offer(neighbor);
                        }
                        chars[i] = node.charAt(i); // Reset
                    }
                }
            }
            depth++;
        }
        return -1;
    }
}
      
/**
 * @param {string[]} deadends
 * @param {string} target
 * @return {number}
 */
var openLock = function(deadends, target) {
    const dead = new Set(deadends);
    const visited = new Set(['0000']);
    if (dead.has('0000')) return -1;
    const queue = [['0000', 0]];
    while (queue.length) {
        const [node, depth] = queue.shift();
        if (node === target) return depth;
        for (let i = 0; i < 4; ++i) {
            let digit = parseInt(node[i]);
            for (let move of [-1, 1]) {
                let newDigit = (digit + move + 10) % 10;
                let neighbor = node.slice(0, i) + newDigit + node.slice(i + 1);
                if (!visited.has(neighbor) && !dead.has(neighbor)) {
                    visited.add(neighbor);
                    queue.push([neighbor, depth + 1]);
                }
            }
        }
    }
    return -1;
};
      

Time and Space Complexity

Brute-force Approach:

  • There are up to 10,000 possible combinations (from "0000" to "9999").
  • Exploring all possible move sequences could take exponential time, as each move can branch to 8 new combinations (2 moves per wheel × 4 wheels).
  • Time complexity: O(8^d), where d is the depth of the shortest path, which is infeasible for large d.
Optimized BFS Approach:
  • Each combination is visited at most once.
  • For each node, we generate 8 neighbors (2 moves per wheel).
  • Time complexity: O(N), where N = 10,000 (all possible combinations).
  • Space complexity: O(N) for the visited set and queue.

The BFS ensures we never revisit nodes, and the use of sets keeps lookups fast.

Summary

The "Open the Lock" problem is a classic shortest-path challenge on an implicit graph. By modeling each lock combination as a node and each valid move as an edge, we use BFS to efficiently find the minimum moves to the target. Key insights include representing deadends and visited nodes with sets for fast lookups, and using BFS to guarantee the shortest solution. This approach is both efficient and elegant, making it suitable for large search spaces with simple move rules.