|
| 1 | +# 752. Open the Lock - Best Practices of LeetCode Solutions |
| 2 | +LeetCode link: [752. Open the Lock](https://leetcode.com/problems/open-the-lock), difficulty: **Medium**. |
| 3 | + |
| 4 | +## LeetCode description of "752. Open the Lock" |
| 5 | +You have a lock in front of you 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. |
| 6 | + |
| 7 | +The lock initially starts at `0000`, a string representing the state of the 4 wheels. |
| 8 | + |
| 9 | +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. |
| 10 | + |
| 11 | +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_. |
| 12 | + |
| 13 | +### [Example 1] |
| 14 | +**Input**: `deadends = ["0201","0101","0102","1212","2002"], target = "0202"` |
| 15 | + |
| 16 | +**Output**: `6` |
| 17 | + |
| 18 | +**Explanation**: |
| 19 | +``` |
| 20 | +A sequence of valid moves would be "0000" -> "1000" -> "1100" -> "1200" -> "1201" -> "1202" -> "0202". |
| 21 | +Note that a sequence like "0000" -> "0001" -> "0002" -> "0102" -> "0202" would be invalid, |
| 22 | +because the wheels of the lock become stuck after the display becomes the dead end "0102". |
| 23 | +``` |
| 24 | + |
| 25 | +### [Example 2] |
| 26 | +**Input**: `deadends = ["8888"], target = "0009"` |
| 27 | + |
| 28 | +**Output**: `1` |
| 29 | + |
| 30 | +**Explanation**: |
| 31 | +``` |
| 32 | +We can turn the last wheel in reverse to move from "0000" -> "0009". |
| 33 | +``` |
| 34 | + |
| 35 | +### [Example 3] |
| 36 | +**Input**: `["8887","8889","8878","8898","8788","8988","7888","9888"], target = "8888"` |
| 37 | + |
| 38 | +**Output**: `-1` |
| 39 | + |
| 40 | +**Explanation**: `We cannot reach the target without getting stuck.` |
| 41 | + |
| 42 | +### [Constraints] |
| 43 | +- `1 <= deadends.length <= 500` |
| 44 | +- `deadends[i].length == 4` |
| 45 | +- `target.length == 4` |
| 46 | +- `target` **will not be** in the list `deadends`. |
| 47 | +- `target` and `deadends[i]` consist of digits only. |
| 48 | + |
| 49 | +### [Hints] |
| 50 | +<details> |
| 51 | + <summary>Hint 1</summary> |
| 52 | + We can think of this problem as a shortest path problem on a graph: there are `10000` nodes (strings `'0000'` to `'9999'`), and there is an edge between two nodes if they differ in one digit, that digit differs by 1 (wrapping around, so `'0'` and `'9'` differ by 1), and if *both* nodes are not in `deadends`. |
| 53 | +</details> |
| 54 | + |
| 55 | +## Intuition |
| 56 | +We can think of this problem as a shortest path problem on a graph: there are `10000` nodes (strings `'0000'` to `'9999'`), and there is an edge between two nodes if they differ in one digit, that digit differs by 1 (wrapping around, so `'0'` and `'9'` differ by 1), and if *both* nodes are not in `deadends`. |
| 57 | + |
| 58 | +### Solution 1: Breadth-First Search |
| 59 | + |
| 60 | + |
| 61 | +* As shown in the figure above, **Breadth-First Search** can be thought of as visiting vertices in rounds and rounds. Actually, whenever you see a question is about |
| 62 | + getting `minimum number` of something of a graph, `Breadth-First Search` would probably help. |
| 63 | + |
| 64 | +* `Breadth-First Search` emphasizes first-in-first-out, so a **queue** is needed. |
| 65 | + |
| 66 | +### Solution 2: A* (A-Star) Algorithm |
| 67 | + |
| 68 | +**A-Star Algorithm** can be used to improve the performance of **Breadth-First Search Algorithm**. |
| 69 | + |
| 70 | +**Breadth-First Search** treats each vertex equally, which inevitably leads to poor performance. |
| 71 | + |
| 72 | +**A-Star Algorithm** calculates the **distance** between each `vertex` and the `target vertex`, and prioritizes vertices with close distances, which greatly improves performance. |
| 73 | + |
| 74 | +#### A* (A-Star) Algorithm Has Two (or Three) Key Actions |
| 75 | +1. Use `priority_queue`. |
| 76 | +2. The function for calculating `distance` should be **carefully designed** (poor design will lead to **subtle** errors in the results). |
| 77 | +3. In special cases, simply using `distance` as the sorting basis for `priority_queue` is not enough, and you need to **make some adjustments**, such as adding it to the `step_count` variable value (not involved in this example). |
| 78 | + |
| 79 | +## Complexity |
| 80 | +> `N` is the length of `deadends`. |
| 81 | +
|
| 82 | +### Solution 1: Breadth-First Search |
| 83 | +* Time: `O(10^4)`. |
| 84 | +* Space: `O(N)`. |
| 85 | + |
| 86 | +### Solution 2: A* (A-Star) Algorithm |
| 87 | +* Time: `O(5 * 4 * 4 * 2 + N)`. |
| 88 | +* Space: `O(N)`. |
| 89 | + |
| 90 | +## Python |
| 91 | +### Solution 1: Breadth-First Search |
| 92 | +```python |
| 93 | +class Solution: |
| 94 | + def openLock(self, deadends: List[str], target_digits: str) -> int: |
| 95 | + if '0000' == target_digits: |
| 96 | + return 0 |
| 97 | + |
| 98 | + self.deadends = set(deadends) |
| 99 | + if '0000' in deadends: |
| 100 | + return -1 |
| 101 | + |
| 102 | + self.queue = deque(['0000']) |
| 103 | + self.deadends.add('0000') |
| 104 | + self.target_digits = target_digits |
| 105 | + result = 0 |
| 106 | + |
| 107 | + while self.queue: |
| 108 | + result += 1 |
| 109 | + queue_size = len(self.queue) |
| 110 | + |
| 111 | + for _ in range(queue_size): |
| 112 | + digits = self.queue.popleft() |
| 113 | + |
| 114 | + if self.turn_one_slot(digits): |
| 115 | + return result |
| 116 | + |
| 117 | + return -1 |
| 118 | + |
| 119 | + def turn_one_slot(self, digits): |
| 120 | + for i in range(len(digits)): |
| 121 | + for digit in closest_digits(int(digits[i])): |
| 122 | + new_digits = f'{digits[:i]}{digit}{digits[i + 1:]}' |
| 123 | + |
| 124 | + if new_digits == self.target_digits: |
| 125 | + return True |
| 126 | + |
| 127 | + if new_digits in self.deadends: |
| 128 | + continue |
| 129 | + |
| 130 | + self.queue.append(new_digits) |
| 131 | + self.deadends.add(new_digits) |
| 132 | + |
| 133 | + |
| 134 | +def closest_digits(digit): |
| 135 | + if digit == 0: |
| 136 | + return (9, 1) |
| 137 | + |
| 138 | + if digit == 9: |
| 139 | + return (8, 0) |
| 140 | + |
| 141 | + return (digit - 1, digit + 1) |
| 142 | +``` |
| 143 | + |
| 144 | +### Solution 2: A* (A-Star) Algorithm |
| 145 | +```python |
| 146 | +class Solution: |
| 147 | + def openLock(self, deadends: List[str], target_digits: str) -> int: |
| 148 | + if '0000' == target_digits: |
| 149 | + return 0 |
| 150 | + |
| 151 | + deadends = set(deadends) |
| 152 | + if '0000' in deadends: |
| 153 | + return -1 |
| 154 | + |
| 155 | + self.target_digits = target_digits |
| 156 | + |
| 157 | + priority_queue = [(self.distance('0000'), '0000', 0)] |
| 158 | + |
| 159 | + while priority_queue: |
| 160 | + _, digits, turns = heapq.heappop(priority_queue) |
| 161 | + |
| 162 | + for i in range(4): |
| 163 | + for digit in closest_digits(int(digits[i])): |
| 164 | + new_digits = f'{digits[:i]}{digit}{digits[i + 1:]}' |
| 165 | + |
| 166 | + if new_digits == target_digits: |
| 167 | + return turns + 1 |
| 168 | + |
| 169 | + if new_digits in deadends: |
| 170 | + continue |
| 171 | + |
| 172 | + heapq.heappush( |
| 173 | + priority_queue, |
| 174 | + (self.distance(new_digits), new_digits, turns + 1) |
| 175 | + ) |
| 176 | + deadends.add(new_digits) |
| 177 | + |
| 178 | + return -1 |
| 179 | + |
| 180 | + def distance(self, digits): |
| 181 | + result = 0 |
| 182 | + |
| 183 | + # 0 1 2 3 4 5 6 7 8 9 |
| 184 | + # 0 1 2 3 4 5 6 7 8 9 |
| 185 | + # 0 1 2 3 4 5 6 7 8 9 |
| 186 | + # 0 1 2 3 4 5 6 7 8 9 |
| 187 | + for i in range(4): |
| 188 | + # 'Euclidean Distance' is widely used in 'A* Algorithm'. |
| 189 | + result += abs(int(self.target_digits[i]) - int(digits[i])) ** 2 |
| 190 | + |
| 191 | + return result ** 0.5 |
| 192 | + |
| 193 | + |
| 194 | +def closest_digits(digit): |
| 195 | + if digit == 0: |
| 196 | + return (9, 1) |
| 197 | + |
| 198 | + if digit == 9: |
| 199 | + return (8, 0) |
| 200 | + |
| 201 | + return (digit - 1, digit + 1) |
| 202 | +``` |
| 203 | + |
| 204 | +## JavaScript |
| 205 | +```javascript |
| 206 | +// Welcome to create a PR to complete the code of this language, thanks! |
| 207 | +``` |
| 208 | + |
| 209 | +## Java |
| 210 | +```java |
| 211 | +// Welcome to create a PR to complete the code of this language, thanks! |
| 212 | +``` |
| 213 | + |
| 214 | +## C++ |
| 215 | +```cpp |
| 216 | +// Welcome to create a PR to complete the code of this language, thanks! |
| 217 | +``` |
| 218 | + |
| 219 | +## C# |
| 220 | +```c# |
| 221 | +// Welcome to create a PR to complete the code of this language, thanks! |
| 222 | +``` |
| 223 | + |
| 224 | +## Go |
| 225 | +```go |
| 226 | +// Welcome to create a PR to complete the code of this language, thanks! |
| 227 | +``` |
| 228 | + |
| 229 | +## Ruby |
| 230 | +```ruby |
| 231 | +# Welcome to create a PR to complete the code of this language, thanks! |
| 232 | +``` |
| 233 | + |
| 234 | +## C, Kotlin, Swift, Rust or other languages |
| 235 | +``` |
| 236 | +// Welcome to create a PR to complete the code of this language, thanks! |
| 237 | +``` |
0 commit comments