给定一个二维网格 grid
,其中:
- '.' 代表一个空房间
- '#' 代表一堵
- '@' 是起点
- 小写字母代表钥匙
- 大写字母代表锁
我们从起点开始出发,一次移动是指向四个基本方向之一行走一个单位空间。我们不能在网格外面行走,也无法穿过一堵墙。如果途经一个钥匙,我们就把它捡起来。除非我们手里有对应的钥匙,否则无法通过锁。
假设 k 为 钥匙/锁 的个数,且满足 1 <= k <= 6
,字母表中的前 k
个字母在网格中都有自己对应的一个小写和一个大写字母。换言之,每个锁有唯一对应的钥匙,每个钥匙也有唯一对应的锁。另外,代表钥匙和锁的字母互为大小写并按字母顺序排列。
返回获取所有钥匙所需要的移动的最少次数。如果无法获取所有钥匙,返回 -1
。
示例 1:
输入:grid = ["@.a.#","###.#","b.A.B"]
输出:8
解释:目标是获得所有钥匙,而不是打开所有锁。
示例 2:
输入:grid = ["@..aA","..B#.","....b"]
输出:6
示例 3:
输入: grid = ["@Aa"] 输出: -1
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 30
grid[i][j]
只含有'.'
,'#'
,'@'
,'a'-
'f
'
以及'A'-'F'
- 钥匙的数目范围是
[1, 6]
- 每个钥匙都对应一个 不同 的字母
- 每个钥匙正好打开一个对应的锁
分析:看起来本题是一个遍历题,然而问题在于所有的位置并不是遍历一次之后就不能遍历了,然而两次有意义的遍历之间,即使是同一个位置的遍历,也存在不同,现在我们来看看这种不同。我们改造一下示例1为如下情形:
图1
图上红线为正确的答案路径,而我们可以观察到红线两次经过了A处,然而两次经过的状态是有区别的,第一次经过A处,是持有a钥匙的,而第二次经过A处,是持有a、b钥匙的,因此我们发现,判断这个点是否可达,不仅仅要看位置,还要看持有钥匙的状态。由于钥匙存在两种状态-持有或者未持有,且最多6把钥匙,我们可以用状态压缩,使用6位2进制来表示钥匙是否持有。同时,为了保证我们拿到钥匙的路径是最短的,我们可以使用BFS,那么解法就来了-状态压缩的BFS。
现在我们来考虑一下问题:
使用visited[x][y][state]表示处于(x,y)位置且钥匙持有状态为state时对应的状态是否访问过。我们访问邻接点的准则为:设当前所在点位置为(x,y),状态为state,遍历到周围邻接的点(nx, ny)后状态为next_state(因为遍历到新钥匙所在点状态可能发生变化),那么能够访问的条件是visited[nx][ny][next_state]=false。
我们考量一下上述标准,考察一下遍历到的邻接点的情况:
'#':是墙,没有办法走,不用考虑
'@'/‘.’:普通方格,走到这个方格不可能引起任何状态的变化,但是当前和上一次经过这个方格的时候钥匙的持有状态可能不同,如图1所示,@和(0,1)位置的.都经过了两次,但两次钥匙的持有状态不同,很明显,如果相同的钥匙持有状态经过这些位置并不会得到更优的答案。综上就是要求对应的visited[x][y][state]=false。
'a'-'z':钥匙,因为我们不会拿两次钥匙,但是确实有可能当前和上一次经过这个方格的时候钥匙的持有状态可能不同,也就是说,当我们拿过一次钥匙后,可以把这个位置看作普通方格,但是请注意,看作普通方格的前提是拿过一次钥匙后,也就是状态必须是拥有这个位置的钥匙的状态,因此我们不能去改变grid方格里的字符值。综上就是要求对应的visited[x][y][state]=false。
'A'-'Z':门,门位置能够经过的必要条件是拥有钥匙,并且在本题的背景下,打开一次的门也可以视为普通方格,同时也要求对应的visited[x][y][state]=false。
class Solution {
public:
bool has_key[26];
int n, m;
struct bfs_node{
int x, y, step, key_state;
bfs_node(int sx, int sy, int s_step, int s_state){
x = sx;
y = sy;
step = s_step;
key_state = s_state;
}
};
int shortestPathAllKeys(vector<string>& grid) {
// 确定起点和钥匙的个数
int i, j, sx, sy, key_num = 0;
n = grid.size(), m = grid[0].size();
for(int i = 0; i < n; ++ i){
for(int j = 0; j < m; ++ j){
if(grid[i][j] == '@'){
sx = i;
sy = j;
}else if(grid[i][j] >= 'a' && grid[i][j] <= 'z'){
++ key_num;
}
}
}
bool visited[n + 5][m + 5][1 << key_num];
memset(visited, false, sizeof(visited));
int target_state = (1 << key_num) - 1, next_state, key_bit;
// 带状态窥探的BFS 如果以相同的状态经过同一个位置 肯定是有问题的
queue<bfs_node> que;
bfs_node node = bfs_node{sx, sy, 0, 0};
visited[sx][sy][0] = true;
que.push(node);
int dir[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}, dir_idx, nx, ny;
while(!que.empty()){
node = que.front();
que.pop();
if(node.key_state == target_state){
return node.step;
}
for(dir_idx = 0; dir_idx < 4; ++ dir_idx){
nx = dir[dir_idx][0] + node.x;
ny = dir[dir_idx][1] + node.y;
if(nx >= n || nx < 0 || ny >= m || ny < 0) continue;//越界
if(grid[nx][ny] >= 'a' && grid[nx][ny] <= 'z'){//钥匙的位置
key_bit = 1 << (grid[nx][ny] - 'a');
next_state = node.key_state | key_bit;
if(!visited[nx][ny][next_state]){
que.push(bfs_node{nx, ny, node.step + 1, next_state});
visited[nx][ny][next_state] = true;
}
}else if(grid[nx][ny] >= 'A' && grid[nx][ny] <= 'Z'){//有锁的位置
key_bit = 1 << (grid[nx][ny] - 'A');
if(node.key_state & key_bit && !visited[nx][ny][node.key_state]){//可以打开锁
que.push(bfs_node{nx, ny, node.step + 1, node.key_state});
visited[nx][ny][node.key_state] = true;
}
}else if(grid[nx][ny] != '#' && !visited[nx][ny][node.key_state]){//空房间
que.push(bfs_node{nx, ny, node.step + 1, node.key_state});
visited[nx][ny][node.key_state] = true;
}
}
}
return -1;
}
};