|
| 1 | +### 题目描述 |
| 2 | + |
| 3 | +这是 LeetCode 上的 **[864. 获取所有钥匙的最短路径](https://leetcode.cn/problems/shortest-path-to-get-all-keys/solution/by-ac_oier-5gxc/)** ,难度为 **困难**。 |
| 4 | + |
| 5 | +Tag : 「BFS」、「状态压缩」 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +给定一个二维网格 grid ,其中: |
| 10 | + |
| 11 | +* `'.'` 代表一个空房间 |
| 12 | +* `'#'` 代表一堵墙 |
| 13 | +* `'@'` 是起点 |
| 14 | +* 小写字母代表钥匙 |
| 15 | +* 大写字母代表锁 |
| 16 | + |
| 17 | +我们从起点开始出发,一次移动是指向四个基本方向之一行走一个单位空间。我们不能在网格外面行走,也无法穿过一堵墙。如果途经一个钥匙,我们就把它捡起来。除非我们手里有对应的钥匙,否则无法通过锁。 |
| 18 | + |
| 19 | +假设 `k` 为 钥匙/锁 的个数,且满足 1ドル <= k <= 6,ドル字母表中的前 `k` 个字母在网格中都有自己对应的一个小写和一个大写字母。换言之,每个锁有唯一对应的钥匙,每个钥匙也有唯一对应的锁。另外,代表钥匙和锁的字母互为大小写并按字母顺序排列。 |
| 20 | + |
| 21 | +返回获取所有钥匙所需要的移动的最少次数。如果无法获取所有钥匙,返回 `-1` 。 |
| 22 | + |
| 23 | +示例 1: |
| 24 | + |
| 25 | +``` |
| 26 | +输入:grid = ["@.a.#","###.#","b.A.B"] |
| 27 | + |
| 28 | +输出:8 |
| 29 | + |
| 30 | +解释:目标是获得所有钥匙,而不是打开所有锁。 |
| 31 | +``` |
| 32 | +示例 2: |
| 33 | + |
| 34 | +``` |
| 35 | +输入:grid = ["@..aA","..B#.","....b"] |
| 36 | + |
| 37 | +输出:6 |
| 38 | +``` |
| 39 | +示例 3: |
| 40 | + |
| 41 | +``` |
| 42 | +输入: grid = ["@Aa"] |
| 43 | + |
| 44 | +输出: -1 |
| 45 | +``` |
| 46 | + |
| 47 | +提示: |
| 48 | +* $m == grid.length$ |
| 49 | +* $n == grid[i].length$ |
| 50 | +* 1ドル <= m, n <= 30$ |
| 51 | +* `grid[i][j]` 只含有 `'.'`,`'#'`, `'@'`, `'a'-'f'` 以及 `'A'-'F'` |
| 52 | +* 钥匙的数目范围是 $[1, 6]$ |
| 53 | +* 每个钥匙都对应一个 不同 的字母 |
| 54 | +* 每个钥匙正好打开一个对应的锁 |
| 55 | + |
| 56 | +--- |
| 57 | + |
| 58 | +### BFS + 状态压缩 |
| 59 | + |
| 60 | +**一道常规的 `BFS` 运用题,只不过需要在 `BFS` 过程中记录收集到的钥匙状态。** |
| 61 | + |
| 62 | +利用「钥匙数量不超过 6ドル,ドル并按字母顺序排列」,我们可以使用一个 `int` 类型二进制数 `state` 来代指当前收集到钥匙情况: |
| 63 | + |
| 64 | +* 若 `state` 的二进制中的第 $k$ 位为 `1`,代表当前种类编号为 $k$ 的钥匙 **已被收集**,后续移动若遇到对应的锁则 **能通过** |
| 65 | +* 若 `state` 的二进制中的第 $k$ 位为 `0`,代表当前种类编号为 $k$ 的钥匙 **未被收集**,后续移动若遇到对应的锁则 **无法通过** |
| 66 | + |
| 67 | +其中「钥匙种类编号」则按照小写字母先后顺序,从 0ドル$ 开始进行划分对应:即字符为 `a` 的钥匙编号为 `0`,字符为 `b` 的钥匙编号为 `1`,字符为 `c` 的钥匙编号为 `2` ... |
| 68 | + |
| 69 | +当使用了这样的「状态压缩」技巧后,我们可以很方便通过「位运算」进行 **钥匙检测** 和 **更新钥匙收集状态**: |
| 70 | + |
| 71 | +* 钥匙检测:`(state >> k) & 1`,若返回 `1` 说明第 $k$ 位为 `1`,当前持有种类编号为 `k` 的钥匙 |
| 72 | +* 更新钥匙收集状态:`state |= 1 << k`,将 `state` 的第 $k$ 位设置为 `1`,代表当前新收集到种类编号为 `k` 的钥匙 |
| 73 | + |
| 74 | +搞明白如何记录当前收集到的钥匙状态后,剩下的则是常规 `BFS` 过程: |
| 75 | + |
| 76 | +1. 起始遍历一次棋盘,找到起点位置,并将其进行入队,队列维护的是 $(x, y, state)$ 三元组状态(其中 $(x, y)$ 代表当前所在的棋盘位置,$state$ 代表当前的钥匙收集情况) |
| 77 | + 同时统计整个棋盘所包含的钥匙数量 `cnt`,并使用 数组/哈希表 记录到达每个状态所需要消耗的最小步数 `step` |
| 78 | + |
| 79 | +2. 进行四联通方向的 `BFS`,转移过程中需要注意「遇到锁时,必须有对应钥匙才能通过」&「遇到钥匙时,需要更新对应的 `state` 再进行入队」 |
| 80 | + |
| 81 | +3. 当 `BFS` 过程中遇到 `state = (1 << cnt) - 1` 时,代表所有钥匙均被收集完成,可结束搜索 |
| 82 | + |
| 83 | +Java 代码: |
| 84 | +```Java |
| 85 | +class Solution { |
| 86 | + static int N = 35, K = 10, INF = 0x3f3f3f3f; |
| 87 | + static int[][][] dist = new int[N][N][1 << K]; |
| 88 | + static int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}}; |
| 89 | + public int shortestPathAllKeys(String[] g) { |
| 90 | + int n = g.length, m = g[0].length(), cnt = 0; |
| 91 | + Deque<int[]> d = new ArrayDeque<>(); |
| 92 | + for (int i = 0; i < n; i++) { |
| 93 | + for (int j = 0; j < m; j++) { |
| 94 | + Arrays.fill(dist[i][j], INF); |
| 95 | + char c = g[i].charAt(j); |
| 96 | + if (c == '@') { |
| 97 | + d.addLast(new int[]{i, j, 0}); |
| 98 | + dist[i][j][0] = 0; |
| 99 | + } else if (c >= 'a' && c <= 'z') cnt++; |
| 100 | + } |
| 101 | + } |
| 102 | + while (!d.isEmpty()) { |
| 103 | + int[] info = d.pollFirst(); |
| 104 | + int x = info[0], y = info[1], cur = info[2], step = dist[x][y][cur]; |
| 105 | + for (int[] di : dirs) { |
| 106 | + int nx = x + di[0], ny = y + di[1]; |
| 107 | + if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue; |
| 108 | + char c = g[nx].charAt(ny); |
| 109 | + if (c == '#') continue; |
| 110 | + if ((c >= 'A' && c <= 'Z') && (cur >> (c - 'A') & 1) == 0) continue; |
| 111 | + int ncur = cur; |
| 112 | + if (c >= 'a' && c <= 'z') ncur |= 1 << (c - 'a'); |
| 113 | + if (ncur == (1 << cnt) - 1) return step + 1; |
| 114 | + if (step + 1 >= dist[nx][ny][ncur]) continue; |
| 115 | + dist[nx][ny][ncur] = step + 1; |
| 116 | + d.addLast(new int[]{nx, ny, ncur}); |
| 117 | + } |
| 118 | + } |
| 119 | + return -1; |
| 120 | + } |
| 121 | +} |
| 122 | +``` |
| 123 | +TypeScript 代码: |
| 124 | +```TypeScript |
| 125 | +function shortestPathAllKeys(g: string[]): number { |
| 126 | + const dirs = [[1,0],[-1,0],[0,1],[0,-1]] |
| 127 | + let n = g.length, m = g[0].length, cnt = 0 |
| 128 | + const dist = new Array<Array<Array<number>>>() |
| 129 | + for (let i = 0; i < n; i++) { |
| 130 | + dist[i] = new Array<Array<number>>(m) |
| 131 | + for (let j = 0; j < m; j++) { |
| 132 | + dist[i][j] = new Array<number>(1 << 10).fill(0x3f3f3f3f) |
| 133 | + } |
| 134 | + } |
| 135 | + const d = [] |
| 136 | + for (let i = 0; i < n; i++) { |
| 137 | + for (let j = 0; j < m; j++) { |
| 138 | + if (g[i][j] == '@') { |
| 139 | + d.push([i, j, 0]); dist[i][j][0] = 0 |
| 140 | + } else if (g[i][j] >= 'a' && g[i][j] <= 'z') cnt++ |
| 141 | + } |
| 142 | + } |
| 143 | + while (d.length > 0) { |
| 144 | + const info = d.shift() |
| 145 | + const x = info[0], y = info[1], cur = info[2], step = dist[x][y][cur] |
| 146 | + for (const di of dirs) { |
| 147 | + const nx = x + di[0], ny = y + di[1] |
| 148 | + if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue |
| 149 | + const c = g[nx][ny] |
| 150 | + if (c == '#') continue |
| 151 | + if ('A' <= c && c <= 'Z' && ((cur >> (c.charCodeAt(0) - 'A'.charCodeAt(0)) & 1) == 0)) continue |
| 152 | + let ncur = cur |
| 153 | + if ('a' <= c && c <= 'z') ncur |= 1 << (c.charCodeAt(0) - 'a'.charCodeAt(0)) |
| 154 | + if (ncur == (1 << cnt) - 1) return step + 1 |
| 155 | + if (step + 1 >= dist[nx][ny][ncur]) continue |
| 156 | + d.push([nx, ny, ncur]) |
| 157 | + dist[nx][ny][ncur] = step + 1 |
| 158 | + } |
| 159 | + } |
| 160 | + return -1 |
| 161 | +} |
| 162 | +``` |
| 163 | +Python3 代码: |
| 164 | +```Python |
| 165 | +class Solution: |
| 166 | + def shortestPathAllKeys(self, g: List[str]) -> int: |
| 167 | + dirs = [[0,1], [0,-1], [1,0], [-1,0]] |
| 168 | + n, m, cnt = len(g), len(g[0]), 0 |
| 169 | + dist = defaultdict(lambda : 0x3f3f3f3f) |
| 170 | + for i in range(n): |
| 171 | + for j in range(m): |
| 172 | + c = g[i][j] |
| 173 | + if c == '@': |
| 174 | + d = deque([(i, j, 0)]) |
| 175 | + dist[(i, j, 0)] = 0 |
| 176 | + elif 'a' <= c <= 'z': |
| 177 | + cnt += 1 |
| 178 | + while d: |
| 179 | + x, y, cur = d.popleft() |
| 180 | + step = dist[(x, y, cur)] |
| 181 | + for di in dirs: |
| 182 | + nx, ny = x + di[0], y + di[1] |
| 183 | + if nx < 0 or nx >= n or ny < 0 or ny >= m: |
| 184 | + continue |
| 185 | + c = g[nx][ny] |
| 186 | + if c == '#': |
| 187 | + continue |
| 188 | + if 'A' <= c <= 'Z' and (cur >> (ord(c) - ord('A')) & 1) == 0: |
| 189 | + continue |
| 190 | + ncur = cur |
| 191 | + if 'a' <= c <= 'z': |
| 192 | + ncur |= (1 << (ord(c) - ord('a'))) |
| 193 | + if ncur == (1 << cnt) - 1: |
| 194 | + return step + 1 |
| 195 | + if step + 1 >= dist[(nx, ny, ncur)]: |
| 196 | + continue |
| 197 | + dist[(nx, ny, ncur)] = step + 1 |
| 198 | + d.append((nx, ny, ncur)) |
| 199 | + return -1 |
| 200 | +``` |
| 201 | +* 时间复杂度:$O(n \times m \times 2^k)$ |
| 202 | +* 空间复杂度:$O(n \times m \times 2^k)$ |
| 203 | + |
| 204 | +--- |
| 205 | + |
| 206 | +### 最后 |
| 207 | + |
| 208 | +这是我们「刷穿 LeetCode」系列文章的第 `No.864` 篇,系列开始于 2021年01月01日,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 |
| 209 | + |
| 210 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 |
| 211 | + |
| 212 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 |
| 213 | + |
| 214 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 |
| 215 | + |
0 commit comments