文章目录
题目
标题和出处
标题:获取所有钥匙的最短路径
难度
8 级
题目描述
要求
给定一个 m × n \texttt{m} \times \texttt{n} m×n 的二维网格 grid \texttt{grid} grid,其中:
- '.' \texttt{`.'} '.' 是空单元格
- '#' \texttt{`\#'} '#' 是墙
- '@' \texttt{`@'} '@' 是起点
- 小写字母表示钥匙
- 大写字母表示锁
从起点出发,一次移动可以向水平或竖直的四个方向之一行走一个单位空间。不能走到网格外面或通过墙。
如果走到钥匙的位置,就捡起钥匙。只有当拥有和锁对应的钥匙时才能通过锁。
对于 1 ≤ k ≤ 6 \texttt{1} \le \texttt{k} \le \texttt{6} 1≤k≤6,网格中恰好有前 k \texttt{k} k 个英语字母的一个小写和一个大写字母。这意味着每个锁有唯一对应的钥匙,每个钥匙有唯一对应的锁。如果锁和钥匙对应,则表示锁和钥匙的字母互为大小写。
返回获取所有钥匙所需要的移动的最少次数。如果无法获取所有钥匙,返回 -1 \texttt{-1} -1。
示例
示例 1:

输入: grid = "@.a.#","###.#","b.A.B" \texttt{grid = "@.a.\\#","\\#\\#\\#.\\#","b.A.B"} grid = "@.a.#","###.#","b.A.B"
输出: 8 \texttt{8} 8
解释:目标是获得所有钥匙,而不是打开所有锁。
示例 2:

输入: grid = "@..aA","..B#.","....b" \texttt{grid = "@..aA","..B\\#.","....b"} grid = "@..aA","..B#.","....b"
输出: 6 \texttt{6} 6
示例 3:

输入: grid = "@Aa" \texttt{grid = "@Aa"} grid = "@Aa"
输出: -1 \texttt{-1} -1
数据范围
- m = grid.length \texttt{m} = \texttt{grid.length} m=grid.length
- n = gridi.length \texttt{n} = \texttt{gridi.length} n=gridi.length
- 1 ≤ m, n ≤ 30 \texttt{1} \le \texttt{m, n} \le \texttt{30} 1≤m, n≤30
- gridij \texttt{gridij} gridij 是英语字母、 '.' \texttt{`.'} '.'、 '#' \texttt{`\#'} '#' 或 '@' \texttt{`@'} '@'
- 钥匙的数目范围是 1, 6 \texttt{1, 6} 1, 6
- 每个钥匙都对应一个不同的字母
- 每个钥匙正好打开一个对应的锁
解法
思路和算法
计算最短路径可以使用广度优先搜索实现。这道题要求计算获取所有钥匙的最短路径的长度,因此广度优先搜索的状态包括当前位置与已获取钥匙集合。
如果一条路径多次经过同一个状态,则可以将路径中经过同一个状态多次的部分去掉而不改变已获取钥匙集合,因此最短路径一定不会多次经过同一个状态。广度优先搜索中需要确保每个状态只访问一次。
由于钥匙数量 k k k 满足 1 ≤ k ≤ 6 1 \le k \le 6 1≤k≤6,因此可以使用 k k k 位二进制数表示已获取钥匙集合,二进制数的从低到高第 0 0 0 位到第 k − 1 k - 1 k−1 位分别表示第 0 0 0 个钥匙到第 k − 1 k - 1 k−1 个钥匙是否已获取,字母 a \text{a} a 到字母 f \text{f} f 分别表示第 0 0 0 个钥匙到第 5 5 5 个钥匙。
起点状态为位于网格中的起点且没有获取任何钥匙,从起点状态开始广度优先搜索,遍历过程中,对于每个状态考虑从当前状态向四个方向移动一个单元格到达的相邻状态,对于可到达且未访问的相邻状态,需要访问相邻状态。可到达的相邻状态和操作如下。
-
如果移动到的单元格是钥匙,则捡起钥匙,将该钥匙加到已获取钥匙集合中。如果已经捡起该钥匙,则已获取钥匙集合不变。可以使用位运算实现已获取钥匙集合的更新。
-
如果移动到的单元格是锁,则只有当拥有和锁对应的钥匙时才能移动到该单元格,已获取钥匙集合不变。
-
如果移动到的单元格是空单元格或起点,则可以移动到该单元格,已获取钥匙集合不变。
当遍历过程中遇到已获取钥匙集合为所有钥匙时,即可得到获取所有钥匙的最短路径的长度,返回路径长度。
如果遍历结束之后已获取钥匙集合仍不是所有钥匙,则返回 − 1 -1 −1。
代码
java
class Solution {
public static final char START = '@', EMPTY = '.', WALL = '#';
private static int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
public int shortestPathAllKeys(String[] grid) {
int m = grid.length, n = grid[0].length();
int totalKeys = 0;
int startRow = -1, startCol = -1;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
char c = grid[i].charAt(j);
if (c == START) {
startRow = i;
startCol = j;
} else if (Character.isLowerCase(c)) {
totalKeys++;
}
}
}
int targetKeys = (1 << totalKeys) - 1;
int[][][] distances = new int[m][n][1 << totalKeys];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
Arrays.fill(distances[i][j], Integer.MAX_VALUE);
}
}
distances[startRow][startCol][0] = 0;
Queue<int[]> queue = new ArrayDeque<int[]>();
queue.offer(new int[]{startRow, startCol, 0});
while (!queue.isEmpty()) {
int[] state = queue.poll();
int row = state[0], col = state[1], mask = state[2];
if (mask == targetKeys) {
return distances[row][col][mask];
}
for (int[] dir : dirs) {
int newRow = row + dir[0], newCol = col + dir[1];
if (newRow >= 0 && newRow < m && newCol >= 0 && newCol < n && grid[newRow].charAt(newCol) != WALL && distances[newRow][newCol][mask] == Integer.MAX_VALUE) {
char c = grid[newRow].charAt(newCol);
if (Character.isLowerCase(c)) {
int index = c - 'a';
int newMask = mask | (1 << index);
distances[newRow][newCol][newMask] = distances[row][col][mask] + 1;
queue.offer(new int[]{newRow, newCol, newMask});
} else if (Character.isUpperCase(c)) {
int index = c - 'A';
if ((mask & (1 << index)) != 0) {
distances[newRow][newCol][mask] = distances[row][col][mask] + 1;
queue.offer(new int[]{newRow, newCol, mask});
}
} else {
distances[newRow][newCol][mask] = distances[row][col][mask] + 1;
queue.offer(new int[]{newRow, newCol, mask});
}
}
}
}
return -1;
}
}
复杂度分析
-
时间复杂度: O ( m n × 2 k ) O(mn \times 2^k) O(mn×2k),其中 m m m 和 n n n 分别是网格的行数和列数, k k k 是钥匙数。广度优先搜索的状态数是 m n × 2 k mn \times 2^k mn×2k,每个状态最多访问一次,每个状态的处理时间是 O ( 1 ) O(1) O(1),因此时间复杂度是 O ( m n × 2 k ) O(mn \times 2^k) O(mn×2k)。
-
空间复杂度: O ( m n × 2 k ) O(mn \times 2^k) O(mn×2k),其中 m m m 和 n n n 分别是网格的行数和列数, k k k 是钥匙数。广度优先搜索的状态数是 m n × 2 k mn \times 2^k mn×2k,记录路径长度的三维数组和队列的空间取决于状态数。
拓展问题
问题描述
原始问题只有计算获取所有钥匙的最短路径的长度,在原始问题的基础上,可以提出拓展问题:获取所有钥匙的最短路径是什么?如果有多种路径,返回其中任意一种。
解法分析
为了得到最短路径,需要记录访问的相邻状态之间的关系,相邻状态指的是从其中一个状态可以经过一次移动到达另一个状态。对于每个状态,其相邻状态包括上一轮状态和下一轮状态,由于同一个状态有一个上一轮状态、一个或多个下一轮状态,为了得到明确的路径,应该记录每个状态的上一轮状态。
需要使用哈希表记录每个状态对应的上一轮状态。从获取所有钥匙的最终状态开始遍历,当到达起点状态时,即可得到最短路径。
代码
下面的代码中,返回值是一个列表,包含从起点到获取所有钥匙的最短路径经过的所有单元格。
java
class Solution {
public static final char START = '@', EMPTY = '.', WALL = '#';
private static int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
int m, n, totalKeys;
public List<int[]> shortestPathAllKeys(String[] grid) {
Map<Integer, Integer> stateMap = new HashMap<Integer, Integer>();
this.m = grid.length;
this.n = grid[0].length();
this.totalKeys = 0;
int startRow = -1, startCol = -1;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
char c = grid[i].charAt(j);
if (c == START) {
startRow = i;
startCol = j;
} else if (Character.isLowerCase(c)) {
totalKeys++;
}
}
}
int targetKeys = (1 << totalKeys) - 1;
int[][][] distances = new int[m][n][1 << totalKeys];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
Arrays.fill(distances[i][j], Integer.MAX_VALUE);
}
}
distances[startRow][startCol][0] = 0;
Queue<int[]> queue = new ArrayDeque<int[]>();
queue.offer(new int[]{startRow, startCol, 0});
while (!queue.isEmpty()) {
int[] state = queue.poll();
int row = state[0], col = state[1], mask = state[2];
int stateHash = getHash(row, col, mask);
if (mask == targetKeys) {
return getPath(row, col, mask, stateMap);
}
for (int[] dir : dirs) {
int newRow = row + dir[0], newCol = col + dir[1];
if (newRow >= 0 && newRow < m && newCol >= 0 && newCol < n && grid[newRow].charAt(newCol) != WALL && distances[newRow][newCol][mask] == Integer.MAX_VALUE) {
char c = grid[newRow].charAt(newCol);
if (Character.isLowerCase(c)) {
int index = c - 'a';
int newMask = mask | (1 << index);
int newHash = getHash(newRow, newCol, newMask);
stateMap.put(newHash, stateHash);
distances[newRow][newCol][newMask] = distances[row][col][mask] + 1;
queue.offer(new int[]{newRow, newCol, newMask});
} else if (Character.isUpperCase(c)) {
int index = c - 'A';
if ((mask & (1 << index)) != 0) {
int newHash = getHash(newRow, newCol, mask);
stateMap.put(newHash, stateHash);
distances[newRow][newCol][mask] = distances[row][col][mask] + 1;
queue.offer(new int[]{newRow, newCol, mask});
}
} else {
int newHash = getHash(newRow, newCol, mask);
stateMap.put(newHash, stateHash);
distances[newRow][newCol][mask] = distances[row][col][mask] + 1;
queue.offer(new int[]{newRow, newCol, mask});
}
}
}
}
return new ArrayList<int[]>();
}
public int getHash(int row, int col, int mask) {
return (row * n + col) * (1 << totalKeys) + mask;
}
public List<int[]> getPath(int endRow, int endCol, int endMask, Map<Integer, Integer> stateMap) {
List<int[]> path = new ArrayList<int[]>();
int hash = getHash(endRow, endCol, endMask);
while (hash >= 0) {
int pos = hash / (1 << totalKeys);
int row = pos / n, col = pos % n;
path.add(new int[]{row, col});
hash = stateMap.getOrDefault(hash, -1);
}
Collections.reverse(path);
return path;
}
}
复杂度分析
-
时间复杂度: O ( m n × 2 k ) O(mn \times 2^k) O(mn×2k),其中 m m m 和 n n n 分别是网格的行数和列数, k k k 是钥匙数。遍历过程中对于每个状态维护上一轮状态的时间是 O ( 1 ) O(1) O(1),路径的长度不超过状态数,因此和原始问题的时间复杂度相同。
-
空间复杂度: O ( m n × 2 k ) O(mn \times 2^k) O(mn×2k),其中 m m m 和 n n n 分别是网格的行数和列数, k k k 是钥匙数。记录上一轮状态的空间取决于状态数,因此和原始问题的空间复杂度相同。