题目描述
迷宫地宫地形信息记录于一维字符串数组 maze 中,maze[i] 为仅由 "."、"X"、"S" 和 "E" 组成的字符串,其中:
- "X" 表示墙,不可直接通行,但可选择将墙推倒后通过;
- "." 表示空地,可以通行;
- "S" 表示迷宫入口;
- "E" 表示迷宫出口。
请你求出入口到出口所需的最少推墙次数。
注意: 迷宫中仅有一个入口和一个出口。
示例 1:
输入:
maze = ["XSXX","....","XXX.","X.E."]
输出: 0
解释:
不需要推倒任何墙就可以直接从入口到达出口,下图是一种可行的方案:
示例 2:
输入:
maze = ["XSXX","X...","XX..","EX.."]
输出: 1
解释:
最少需要推倒 1 次墙,下图是一种可行的方案:
提示:
1 <= maze.length, maze[i].length <= 100
问题分析
这是一个带权重的最短路径问题,具有以下特点:
- 移动规则:
- 移动到空地(".")或出口("E"):代价为 0
- 推倒墙("X")并移动:代价为 1
- 目标:找到从入口("S")到出口("E")的最小推墙次数
- 算法选择:
- 由于只有两种权重(0和1),最适合使用 0-1 BFS
- 也可以使用 Dijkstra 算法
解题思路
0-1 BFS 方法
核心思想:
- 使用双端队列(Deque)
- 代价为0的移动加入队列头部
- 代价为1的移动加入队列尾部
- 这样保证每次取出的都是当前代价最小的状态
算法步骤:
- 找到起点和终点位置
- 使用双端队列进行BFS
- 对于每个位置,尝试向四个方向移动
- 根据目标位置的类型决定代价和入队方式
解法实现
Java 实现
import java.util.ArrayDeque;
import java.util.Deque;
/**
* 3. 走迷宫 II
* 0-1 BFS
*/
class Solution {
// 四个方向:上下左右
private static final int[][] DIRS = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
public int solve(String[] maze) {
int rows = maze.length;
int cols = maze[0].length();
// 找到起点和终点
int startRow = -1, startCol = -1;
int endRow = -1, endCol = -1;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (maze[i].charAt(j) == 'S') {
startRow = i;
startCol = j;
}
if (maze[i].charAt(j) == 'E') {
endRow = i;
endCol = j;
}
}
}
return bfs(maze, startRow, startCol, endRow, endCol);
}
private int bfs(String[] maze, int startRow, int startCol, int endRow, int endCol) {
int rows = maze.length;
int cols = maze[0].length();
// 距离数组,记录到达每个位置的最少推墙次数
int[][] dist = new int[rows][cols];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
dist[i][j] = Integer.MAX_VALUE;
}
}
// 双端队列用于0-1 BFS
Deque<int[]> deque = new ArrayDeque<>();
deque.offer(new int[]{startRow, startCol});
dist[startRow][startCol] = 0;
while (!deque.isEmpty()) {
int[] cur = deque.poll();
int curRow = cur[0];
int curCol = cur[1];
// 如果到达终点,返回结果
if (curRow == endRow && curCol == endCol) {
return dist[curRow][curCol];
}
// 尝试四个方向
for (int[] dir : DIRS) {
int newRow = curRow + dir[0];
int newCol = curCol + dir[1];
// 检查边界
if (newRow < 0 || newRow >= rows || newCol < 0 || newCol >= cols) {
continue;
}
char cell = maze[newRow].charAt(newCol);
int newCost;
// 计算移动代价
if (cell == '.' || cell == 'E') {
newCost = dist[curRow][curCol]; // 移动到空格或终点,代价不变
} else if (cell == 'X') {
newCost = dist[curRow][curCol] + 1; // 移动到墙,代价加1
} else {
continue; // 遇到其他字符,跳过
}
// 如果找到更优路径,更新距离并加入队列
if (newCost < dist[newRow][newCol]) {
dist[newRow][newCol] = newCost;
// 0-1 BFS的关键:根据代价决定加入队列的位置
if (cell == '.' || cell == 'E') {
deque.offerFirst(new int[]{newRow, newCol}); // 代价为0,加入队列头部
} else {
deque.offerLast(new int[]{newRow, newCol}); // 代价为1,加入队列尾部
}
}
}
}
// 无法到达终点
return -1;
}
}
C# 实现
/**
* 3. 走迷宫 II
* 0-1 BFS
*/
public class Solution
{
// 四个方向:上下左右
private int[,] _dirs = new int[,] { { 0, 1 }, { 0, -1 }, { 1, 0 }, { -1, 0 } };
public int Solve(string[] maze)
{
int rows = maze.Length;
int cols = maze[0].Length;
// 找到起点和终点
int startRow = -1, startCol = -1;
int endRow = -1, endCol = -1;
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
if (maze[i][j] == 'S')
{
startRow = i;
startCol = j;
}
else if (maze[i][j] == 'E')
{
endRow = i;
endCol = j;
}
}
}
return Bfs(maze, startRow, startCol, endRow, endCol);
}
private int Bfs(string[] maze, int startRow, int startCol, int endRow, int endCol)
{
int rows = maze.Length;
int cols = maze[0].Length;
// 距离数组,记录到达每个位置的最少推墙次数
int[,] dist = new int[rows, cols];
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
dist[i, j] = int.MaxValue;
}
}
// 双端队列用于0-1 BFS
LinkedList<(int, int)> deque = new LinkedList<(int, int)>();
deque.AddLast((startRow, startCol));
dist[startRow, startCol] = 0;
while (deque.Count > 0)
{
(int, int) cur = deque.First.Value;
deque.RemoveFirst();
int curRow = cur.Item1;
int curCol = cur.Item2;
// 如果到达终点,返回结果
if (curRow == endRow && curCol == endCol)
{
return dist[curRow, curCol];
}
// 尝试四个方向
for (int i = 0; i < 4; i++)
{
int newRow = curRow + _dirs[i, 0];
int newCol = curCol + _dirs[i, 1];
// 检查边界
if (newRow < 0 || newRow >= rows || newCol < 0 || newCol >= cols)
{
continue;
}
char cell = maze[newRow][newCol];
// 计算移动代价
int newCost;
if (cell == '.' || cell == 'E')
{
newCost = dist[curRow, curCol]; // 移动到空格或终点,代价为当前代价
}
else if (cell == 'X')
{
newCost = dist[curRow, curCol] + 1; // 移动到墙,代价为当前代价加1
}
else
{
continue; // 忽略其他字符
}
// 如果找到更优路径,更新距离并加入队列
if (newCost < dist[newRow, newCol])
{
dist[newRow, newCol] = newCost;
// 0-1 BFS的关键:根据代价决定加入队列的位置
if (cell == '.' || cell == 'E')
{
deque.AddFirst((newRow, newCol)); // 移动到空格或终点,加入队列头部
}
else
{
deque.AddLast((newRow, newCol)); // 移动到墙,加入队列尾部
}
}
}
}
return -1;
}
}
算法复杂度分析
时间复杂度: O(M × N)
- M 和 N 分别是迷宫的行数和列数
- 每个位置最多被访问一次
空间复杂度: O(M × N)
- 需要 dist 数组存储到达每个位置的最小代价
- 双端队列在最坏情况下可能存储所有位置