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: 20Example 2:
Input: deadends = ["4443","4445","4434","4454","4344","4544","3444","5444"], target = "4444"
Output: -1Constraints:
1 <= deadends.length <= 500deadends[i].length == 4target.length == 4target will not be in the list deadends.target and deadends[i] consist of digits only.Before attempting this problem, you should be comfortable with:
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.
-1 immediately since we cannot even start.0 turns.8 neighbors (each wheel can go up or down by 1).turns + 1.-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 -1Where is the number of digits , is the number of wheels , and is the number of deadends.
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.
0. If "0000" is a deadend, return -1.0 and process level by level:8 possible moves.-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 -1Where is the number of digits , is the number of wheels , and is the number of deadends.
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.
begin starting from "0000" and end starting from target.begin is larger than end, swap them to always expand the smaller one.begin, generate all 8 neighbors.end, the frontiers meet, so return steps.begin with the temp set.-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 -1Where is the number of digits , is the number of wheels , and is the number of deadends.
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.
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.
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.