Open the Lock
Shortest dial sequence to reach a 4-digit target avoiding deadends — BFS over 10000 states.
5 min read
bfs graph
Problem#
You have a 4-wheel dial starting at “0000”. On each move you can rotate one wheel up or down by 1 (modulo 10). Given a list of forbidden codes (deadends), return the minimum number of moves to reach target, or -1 if impossible.
Examples#
deadends = ["0201","0101","0102","1212","2002"], target = "0202"→6.deadends = ["8888"], target = "0009"→1.deadends = ["0000"], target = "8888"→-1.
Constraints#
1 <= deadends.length <= 500. All strings are 4 digits.
Hints#
Hint 1
Each state has 8 neighbors — each wheel up or down.
Hint 2
BFS gives shortest path in unweighted graphs; treat deadends and visited as the same blocked set.
Approach#
BFS from “0000”. Generate the 8 neighbors by flipping each of the 4 digits up or down modulo 10. Skip deadends and already-seen states.
Solution#
class Solution {public: int openLock(vector<string>& deadends, string target) { unordered_set<string> blocked(deadends.begin(), deadends.end()); if (blocked.count("0000")) return -1; if (target == "0000") return 0; queue<string> q; q.push("0000"); blocked.insert("0000"); int steps = 0; while (!q.empty()) { ++steps; int sz = q.size(); for (int i = 0; i < sz; ++i) { string cur = q.front(); q.pop(); for (int d = 0; d < 4; ++d) { for (int delta : {1, -1}) { string nxt = cur; nxt[d] = static_cast<char>('0' + ((cur[d] - '0' + delta + 10) % 10)); if (blocked.count(nxt)) continue; if (nxt == target) return steps; blocked.insert(nxt); q.push(nxt); } } } } return -1; }};func openLock(deadends []string, target string) int { blocked := make(map[string]bool) for _, d := range deadends { blocked[d] = true } if blocked["0000"] { return -1 } if target == "0000" { return 0 } q := []string{"0000"} blocked["0000"] = true steps := 0 for len(q) > 0 { steps++ sz := len(q) for i := 0; i < sz; i++ { cur := q[0] q = q[1:] for d := 0; d < 4; d++ { for _, delta := range []int{1, -1} { nxt := []byte(cur) nxt[d] = byte('0' + (int(cur[d]-'0')+delta+10)%10) s := string(nxt) if blocked[s] { continue } if s == target { return steps } blocked[s] = true q = append(q, s) } } } } return -1}from collections import dequefrom typing import List
class Solution: def openLock(self, deadends: List[str], target: str) -> int: blocked = set(deadends) if "0000" in blocked: return -1 if target == "0000": return 0 q = deque(["0000"]) blocked.add("0000") steps = 0 while q: steps += 1 for _ in range(len(q)): cur = q.popleft() for d in range(4): for delta in (1, -1): digit = (int(cur[d]) + delta) % 10 nxt = cur[:d] + str(digit) + cur[d + 1:] if nxt in blocked: continue if nxt == target: return steps blocked.add(nxt) q.append(nxt) return -1function openLock(deadends, target) { const blocked = new Set(deadends); if (blocked.has("0000")) return -1; if (target === "0000") return 0; let q = ["0000"]; blocked.add("0000"); let steps = 0; while (q.length > 0) { steps++; const next = []; for (const cur of q) { for (let d = 0; d < 4; d++) { for (const delta of [1, -1]) { const digit = (cur.charCodeAt(d) - 48 + delta + 10) % 10; const nxt = cur.slice(0, d) + digit + cur.slice(d + 1); if (blocked.has(nxt)) continue; if (nxt === target) return steps; blocked.add(nxt); next.push(nxt); } } } q = next; } return -1;}class Solution { public int openLock(String[] deadends, String target) { Set<String> blocked = new HashSet<>(Arrays.asList(deadends)); if (blocked.contains("0000")) return -1; if (target.equals("0000")) return 0; Queue<String> q = new ArrayDeque<>(); q.offer("0000"); blocked.add("0000"); int steps = 0; while (!q.isEmpty()) { steps++; int sz = q.size(); for (int i = 0; i < sz; i++) { String cur = q.poll(); char[] buf = cur.toCharArray(); for (int d = 0; d < 4; d++) { char orig = buf[d]; for (int delta : new int[]{1, -1}) { buf[d] = (char) ('0' + ((orig - '0' + delta + 10) % 10)); String nxt = new String(buf); if (!blocked.contains(nxt)) { if (nxt.equals(target)) return steps; blocked.add(nxt); q.offer(nxt); } } buf[d] = orig; } } } return -1; }}function openLock(deadends: string[], target: string): number { const blocked = new Set<string>(deadends); if (blocked.has("0000")) return -1; if (target === "0000") return 0; let q: string[] = ["0000"]; blocked.add("0000"); let steps = 0; while (q.length > 0) { steps++; const next: string[] = []; for (const cur of q) { for (let d = 0; d < 4; d++) { for (const delta of [1, -1]) { const digit = (cur.charCodeAt(d) - 48 + delta + 10) % 10; const nxt = cur.slice(0, d) + digit + cur.slice(d + 1); if (blocked.has(nxt)) continue; if (nxt === target) return steps; blocked.add(nxt); next.push(nxt); } } } q = next; } return -1;}Editorial#
The state space is exactly 10000, so BFS is trivially affordable. Merging the deadend set with the visited set means we never re-check membership separately. Bidirectional BFS from both "0000" and target would cut work roughly in half — useful when constraints scale up but unnecessary here.
Complexity#
- Time: O(N) where
N = 10000states, each with 8 neighbors. - Space: O(N).
Concept revision#
Related#