LeetCode 1926 - Nearest Exit from Entrance in Maze
The problem gives us a rectangular maze represented by a 2D grid. Each cell contains either: - '.', meaning the cell is empty and can be walked on - '+', meaning the cell is a wall and cannot be crossed We are also given the coordinates of an entrance cell.
Difficulty: 🟡 Medium
Topics: Array, Breadth-First Search, Matrix
Solution
Problem Understanding
The problem gives us a rectangular maze represented by a 2D grid. Each cell contains either:
'.', meaning the cell is empty and can be walked on'+', meaning the cell is a wall and cannot be crossed
We are also given the coordinates of an entrance cell. The entrance is guaranteed to be an empty cell.
From any cell, we may move in four directions:
- Up
- Down
- Left
- Right
Each move costs exactly one step.
The goal is to find the minimum number of steps required to reach the nearest exit. An exit is defined as:
- An empty cell
- Located on the border of the maze
- Not equal to the entrance cell itself
If no reachable exit exists, we return -1.
The important detail is that the entrance does not count as an exit, even if it lies on the border. This condition is a common source of bugs.
The constraints are relatively small:
1 <= m, n <= 100
This means the maze contains at most 10,000 cells. Since this is not extremely large, graph traversal algorithms such as Breadth-First Search are very feasible.
Conceptually, the maze forms an unweighted graph:
- Each empty cell is a node
- Adjacent empty cells are connected by edges
- Every move has equal cost
Because every move costs exactly one step, the shortest path problem naturally suggests Breadth-First Search.
Several edge cases are important:
- The entrance may already lie on the border, but it still cannot be treated as an exit.
- There may be no exits at all.
- There may be exits that are unreachable because walls block the path.
- The maze may consist of only one cell.
- Multiple exits may exist, and we must return the closest one.
Approaches
Brute Force Approach
A brute force strategy would be:
- Identify every valid exit in the maze.
- For each exit, run a separate search from the entrance to determine the shortest path length.
- Return the minimum reachable distance.
This works because eventually every exit is checked, and the shortest reachable one is selected.
However, this approach is inefficient because we repeatedly traverse the maze for every exit. In the worst case, the maze could contain many border cells, and each search could visit nearly all cells.
If there are E exits and m * n cells, the complexity becomes roughly:
O(E * m * n)
Since E can itself be proportional to m + n, this becomes unnecessarily expensive.
Optimal Approach
The key observation is that all moves have equal cost.
Whenever we need the shortest number of moves in an unweighted graph, Breadth-First Search is the ideal tool.
Breadth-First Search explores cells level by level:
- First all cells at distance
1 - Then all cells at distance
2 - Then all cells at distance
3 - And so on
Therefore, the first exit encountered during BFS must be the nearest exit.
Instead of searching separately for every exit, we start one BFS from the entrance and stop immediately when we encounter the first valid exit.
This guarantees optimality while visiting each cell at most once.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(E × m × n) | O(m × n) | Runs a search for every possible exit |
| Optimal | O(m × n) | O(m × n) | Single BFS traversal finds nearest exit immediately |
Algorithm Walkthrough
Step 1: Initialize BFS Structures
We create:
- A queue for BFS traversal
- A visited structure to avoid revisiting cells
The queue stores:
- Current row
- Current column
- Distance from the entrance
We begin by inserting the entrance with distance 0.
Step 2: Mark the Entrance as Visited
We immediately mark the entrance as visited.
This prevents revisiting it later and avoids infinite loops.
Step 3: Define the Four Directions
We define movement vectors for:
- Up
- Down
- Left
- Right
This simplifies neighbor traversal.
Step 4: Process the Queue
While the queue is not empty:
- Remove the front cell
- Explore all four neighboring cells
For each neighbor:
- Check bounds
- Ensure it is not a wall
- Ensure it has not already been visited
If valid:
- Mark it visited
- Compute its distance
- Add it to the queue
Step 5: Detect an Exit
After moving to a valid neighboring cell, check whether:
- It lies on the border
- It is not the entrance
If both conditions hold, we immediately return the current distance plus one.
Because BFS explores in increasing distance order, this is guaranteed to be the nearest exit.
Step 6: Handle No Reachable Exit
If BFS completes without finding any exit, return -1.
Why it works
Breadth-First Search explores nodes in strictly increasing order of distance from the starting point.
This means:
- Every cell reached earlier has a shorter or equal path length than cells reached later.
- The first exit discovered must therefore be the closest exit.
The visited set ensures each cell is processed at most once, preventing cycles and redundant work.
Python Solution
from collections import deque
from typing import List
class Solution:
def nearestExit(self, maze: List[List[str]], entrance: List[int]) -> int:
rows = len(maze)
cols = len(maze[0])
start_row, start_col = entrance
queue = deque()
queue.append((start_row, start_col, 0))
visited = set()
visited.add((start_row, start_col))
directions = [
(-1, 0), # up
(1, 0), # down
(0, -1), # left
(0, 1) # right
]
while queue:
row, col, steps = queue.popleft()
for dr, dc in directions:
new_row = row + dr
new_col = col + dc
# Check boundaries
if (
new_row < 0
or new_row >= rows
or new_col < 0
or new_col >= cols
):
continue
# Skip walls
if maze[new_row][new_col] == '+':
continue
# Skip visited cells
if (new_row, new_col) in visited:
continue
# Check whether this cell is an exit
is_border = (
new_row == 0
or new_row == rows - 1
or new_col == 0
or new_col == cols - 1
)
if is_border:
return steps + 1
visited.add((new_row, new_col))
queue.append((new_row, new_col, steps + 1))
return -1
The implementation begins by determining the maze dimensions and extracting the entrance coordinates.
A deque is used because BFS repeatedly removes elements from the front and appends new elements to the back. Both operations are efficient with a deque.
The visited set prevents revisiting cells. Without it, the search could repeatedly cycle between cells, causing unnecessary work or infinite loops.
Each queue element stores:
- Row index
- Column index
- Current distance from the entrance
Inside the BFS loop, we examine all four neighbors of the current cell.
We skip neighbors that:
- Fall outside the maze
- Are walls
- Were already visited
For every valid unvisited empty cell, we check whether it lies on the border. If it does, we immediately return the path length because BFS guarantees this is the shortest path.
If BFS finishes without finding an exit, the function returns -1.
Go Solution
package main
func nearestExit(maze [][]byte, entrance []int) int {
rows := len(maze)
cols := len(maze[0])
type State struct {
row int
col int
steps int
}
queue := []State{
{entrance[0], entrance[1], 0},
}
visited := make([][]bool, rows)
for i := 0; i < rows; i++ {
visited[i] = make([]bool, cols)
}
visited[entrance[0]][entrance[1]] = true
directions := [][]int{
{-1, 0},
{1, 0},
{0, -1},
{0, 1},
}
for len(queue) > 0 {
current := queue[0]
queue = queue[1:]
for _, dir := range directions {
newRow := current.row + dir[0]
newCol := current.col + dir[1]
// Boundary check
if newRow < 0 || newRow >= rows || newCol < 0 || newCol >= cols {
continue
}
// Wall check
if maze[newRow][newCol] == '+' {
continue
}
// Visited check
if visited[newRow][newCol] {
continue
}
isBorder := (
newRow == 0 ||
newRow == rows-1 ||
newCol == 0 ||
newCol == cols-1
)
if isBorder {
return current.steps + 1
}
visited[newRow][newCol] = true
queue = append(queue, State{
newRow,
newCol,
current.steps + 1,
})
}
}
return -1
}
The Go implementation follows exactly the same BFS logic as the Python solution.
Instead of Python tuples, Go uses a State struct to store:
- Row
- Column
- Step count
The visited structure is implemented using a 2D boolean slice instead of a hash set, which is efficient because the maze dimensions are known in advance.
Go slices naturally support queue behavior by removing the first element using:
queue = queue[1:]
No integer overflow concerns exist here because the maximum path length is bounded by the number of cells, which is at most 10,000.
Worked Examples
Example 1
maze = [
["+","+",".","+"],
[".",".",".","+"],
["+","+","+","."]]
entrance = [1,2]
Initial queue:
| Queue | Visited |
|---|---|
[(1,2,0)] |
{(1,2)} |
Process (1,2):
Possible moves:
- Up →
(0,2)valid and on border - Down → blocked
- Left →
(1,1)valid - Right → blocked
Since (0,2) is a border cell and not the entrance, return:
1
Example 2
maze = [
["+","+","+"],
[".",".","."],
["+","+","+"]]
entrance = [1,0]
Initial state:
| Queue | Visited |
|---|---|
[(1,0,0)] |
{(1,0)} |
Process (1,0):
Neighbors:
(1,1)valid
Queue becomes:
| Queue |
|---|
[(1,1,1)] |
Process (1,1):
Neighbors:
(1,2)valid and on border
Return:
2
Example 3
maze = [[".","+"]]
entrance = [0,0]
The entrance lies on the border, but it does not count as an exit.
No other empty cells exist.
BFS ends immediately.
Return:
-1
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(m × n) | Each cell is visited at most once |
| Space | O(m × n) | Queue and visited structure may store all cells |
The BFS traversal processes each reachable cell once. Every neighbor check is constant time, so the total runtime is proportional to the number of cells in the maze.
The queue and visited structures can both grow to contain every cell in the worst case, giving linear auxiliary space usage.
Test Cases
from typing import List
class Solution:
def nearestExit(self, maze: List[List[str]], entrance: List[int]) -> int:
from collections import deque
rows = len(maze)
cols = len(maze[0])
queue = deque([(entrance[0], entrance[1], 0)])
visited = {(entrance[0], entrance[1])}
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
while queue:
row, col, steps = queue.popleft()
for dr, dc in directions:
nr = row + dr
nc = col + dc
if nr < 0 or nr >= rows or nc < 0 or nc >= cols:
continue
if maze[nr][nc] == '+':
continue
if (nr, nc) in visited:
continue
if nr == 0 or nr == rows - 1 or nc == 0 or nc == cols - 1:
return steps + 1
visited.add((nr, nc))
queue.append((nr, nc, steps + 1))
return -1
sol = Solution()
# Example 1
assert sol.nearestExit(
[["+", "+", ".", "+"],
[".", ".", ".", "+"],
["+", "+", "+", "."]],
[1, 2]
) == 1 # nearest exit directly above
# Example 2
assert sol.nearestExit(
[["+", "+", "+"],
[".", ".", "."],
["+", "+", "+"]],
[1, 0]
) == 2 # entrance on border does not count
# Example 3
assert sol.nearestExit(
[[".", "+"]],
[0, 0]
) == -1 # no exits exist
# Single cell maze
assert sol.nearestExit(
[["."]],
[0, 0]
) == -1 # entrance is only cell
# Exit reachable in one move
assert sol.nearestExit(
[["+", "."],
[".", "."]],
[1, 1]
) == 1 # border exit adjacent
# Multiple exits, choose nearest
assert sol.nearestExit(
[[".", ".", "."],
[".", "+", "."],
[".", ".", "."]],
[1, 0]
) == 1 # nearest border exit
# Completely blocked maze
assert sol.nearestExit(
[["+", "+", "+"],
["+", ".", "+"],
["+", "+", "+"]],
[1, 1]
) == -1 # surrounded by walls
# Larger maze with winding path
assert sol.nearestExit(
[[".", "+", ".", ".", "."],
[".", "+", ".", "+", "."],
[".", ".", ".", "+", "."],
["+", "+", "+", "+", "."],
[".", ".", ".", ".", "."]],
[2, 2]
) == 2 # shortest exit path
print("All tests passed!")
| Test | Why |
|---|---|
| Example 1 | Verifies standard BFS shortest path |
| Example 2 | Ensures entrance is not treated as exit |
| Example 3 | Verifies no-exit scenario |
| Single cell maze | Smallest possible input |
| Adjacent exit | Verifies immediate neighbor detection |
| Multiple exits | Ensures nearest exit is selected |
| Completely blocked maze | Verifies unreachable handling |
| Larger winding maze | Tests more complex traversal |
Edge Cases
Entrance on the Border
One of the most important edge cases occurs when the entrance itself lies on the maze boundary.
A naive implementation might immediately return 0, incorrectly treating the entrance as an exit.
The problem explicitly states that the entrance does not count as an exit. Our implementation handles this correctly because it only checks newly visited neighboring cells for exit status. The entrance itself is never evaluated as a candidate exit.
Maze With No Valid Exit
Another important case is when no reachable border cell exists.
This can happen because:
- All border cells are walls
- Paths are blocked by walls
- The maze contains only the entrance
In this situation, BFS eventually exhausts all reachable cells and the queue becomes empty. The algorithm then correctly returns -1.
Cycles and Revisiting Cells
Open mazes may contain loops where the same cell can be reached through multiple paths.
Without a visited structure, BFS could repeatedly revisit the same cells, causing excessive work or infinite loops.
The implementation prevents this by marking cells visited immediately when they are added to the queue. This guarantees every cell is processed at most once.
Single Row or Single Column Mazes
Very narrow mazes can expose indexing mistakes and border condition bugs.
For example:
[[".", ".", "."]]
Every cell lies on the border.
The implementation safely handles these cases because border detection uses straightforward row and column comparisons that remain valid regardless of maze dimensions.