3552. Grid Teleportation Traversal
Problem Description
You have a 2D grid of characters with m
rows and n
columns. Each cell in the grid contains one of three things:
'.'
- an empty cell you can walk through'#'
- an obstacle you cannot pass through- An uppercase letter
'A'
to'Z'
- a teleportation portal
You start at the top-left corner (0, 0)
and need to reach the bottom-right corner (m-1, n-1)
.
Movement Rules:
-
From any cell, you can move to an adjacent cell (up, down, left, or right) if:
- The destination cell is within the grid boundaries
- The destination cell is not an obstacle
'#'
- Each move to an adjacent cell counts as 1 move
-
When you step on a portal (a cell with a letter), you have the option to instantly teleport to any other cell in the grid that has the same letter:
- Teleportation does not count as a move (it's free)
- Each portal letter can only be used once for teleportation throughout your entire journey
- After using a portal, you cannot teleport using that same letter again
Your task is to find the minimum number of moves needed to reach the destination. If it's impossible to reach the bottom-right corner, return -1
.
For example, if you have two cells with letter 'A'
, when you reach one 'A'
, you can instantly teleport to the other 'A'
for free, but after that, the 'A'
portal is "used up" and cannot be used for teleportation again.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The grid can be viewed as a graph where each cell is a node, and edges connect adjacent cells. Additionally, portals create special edges between cells with the same letter.
Is it a tree?
- No: The grid graph has cycles (you can move in circles) and is not a tree structure.
Is the problem related to directed acyclic graphs?
- No: The grid allows bidirectional movement and contains cycles, so it's not a DAG.
Is the problem related to shortest paths?
- Yes: We need to find the minimum number of moves to reach the destination, which is a shortest path problem.
Is the graph Weighted?
- No/Yes (special case): Regular moves have weight 1, but teleportation has weight 0. This is actually a 0-1 weighted graph, which is a special case that can be solved with modified BFS (0-1 BFS).
Since we have a 0-1 weighted graph (moves cost 1, teleportations cost 0), we arrive at BFS as our solution approach. Specifically, we use 0-1 BFS with a deque:
- When we teleport (cost 0), we add the destination to the front of the deque
- When we make a regular move (cost 1), we add the destination to the back of the deque
Conclusion: The flowchart leads us to use BFS, specifically the 0-1 BFS variant, which is perfect for finding the shortest path in a graph where edges have weights of either 0 or 1.
Intuition
The key insight is recognizing that this problem is essentially a shortest path problem with a twist - we have two types of moves with different "costs":
- Regular moves to adjacent cells (cost = 1 move)
- Teleportation through portals (cost = 0 moves)
This creates a 0-1 weighted graph scenario. In a standard BFS, all edges have the same weight, so we can use a regular queue. But here, since teleportation is "free," we need to prioritize exploring teleported positions before regular moves to ensure we find the optimal path.
Think of it this way: if you're at position A and can either walk to position B (1 move) or teleport to position C (0 moves), you should explore what's reachable from C first, because any path through C will always be shorter than or equal to paths through B.
This is why we use a deque (double-ended queue) instead of a regular queue:
- When we teleport (0 cost), we add the new position to the front of the deque - these positions get processed first
- When we make a regular move (1 cost), we add the new position to the back of the deque
Another crucial observation is that each portal can only be used once. Once we've teleported using letter 'A', we can never use any 'A' portal again. This prevents infinite loops and ensures our algorithm terminates. We track this by removing each portal letter from our dictionary after using it.
The distance tracking ensures we don't revisit cells with a worse path. If we've already reached a cell with distance d
, we only process it again if we can reach it with a smaller distance. This optimization prevents redundant exploration and guarantees we find the shortest path.
Learn more about Breadth-First Search patterns.
Solution Approach
The implementation uses 0-1 BFS with a deque to handle the two different types of moves efficiently. Let's walk through the key components:
Data Structure Setup:
-
Portal Dictionary (
g
): We first scan the entire grid and create a dictionary where each key is a portal letter and the value is a list of all positions containing that letter. This allows O(1) lookup when we need to teleport. -
Distance Matrix (
dist
): A 2D array initialized to infinity that tracks the minimum distance to reach each cell. We setdist[0][0] = 0
as our starting point. -
Deque (
q
): A double-ended queue initialized with the starting position(0, 0)
.
BFS Process: The algorithm processes cells from the deque until either we reach the destination or the queue is empty:
-
Dequeue a cell
(i, j)
and get its current distanced
. -
Check if destination reached: If
i == m-1
andj == n-1
, return the distance. -
Handle portal teleportation: If the current cell contains a portal letter
c
:- Iterate through all other positions with the same letter from
g[c]
- If we can reach any of these positions with a better distance (current distance
d
< stored distance), update the distance and add to the front of the deque usingappendleft()
- Delete the portal letter from the dictionary (
del g[c]
) to ensure it's only used once
- Iterate through all other positions with the same letter from
-
Handle regular moves: Check all four adjacent cells using direction vectors:
- For each valid adjacent cell
(x, y)
that is within bounds, not an obstacle, and can be reached with a better distance (d + 1 < dist[x][y]
): - Update the distance to
d + 1
- Add to the back of the deque using
append()
- For each valid adjacent cell
Why This Works:
- The deque ensures cells reached by teleportation (0 cost) are processed before cells reached by regular moves (cost 1)
- The distance matrix prevents revisiting cells with suboptimal paths
- Deleting used portals from the dictionary ensures each portal is used at most once
- The algorithm guarantees finding the shortest path because it explores all reachable cells in order of their distance from the start
If the queue becomes empty without reaching the destination, the function returns -1
, indicating the destination is unreachable.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach:
Grid (3x3): . A # . # A . . . Start: (0,0) β Goal: (2,2)
Initial Setup:
- Portal dictionary
g = {'A': [(0,1), (1,2)]}
- Distance matrix initialized to infinity, except
dist[0][0] = 0
- Deque
q = [(0,0)]
Step 1: Process (0,0), distance = 0
- Current cell is '.', no portal to use
- Check adjacent cells:
- Right (0,1): 'A', distance 0+1=1 < β, update
dist[0][1]=1
, append to back βq = [(0,1)]
- Down (1,0): '.', distance 0+1=1 < β, update
dist[1][0]=1
, append to back βq = [(0,1), (1,0)]
- Right (0,1): 'A', distance 0+1=1 < β, update
Step 2: Process (0,1), distance = 1
- Current cell is 'A', portal available!
- Teleport to other 'A' at (1,2): distance 1+0=1 < β, update
dist[1][2]=1
, appendleft βq = [(1,2), (1,0)]
- Delete 'A' from portal dictionary:
g = {}
- Check adjacent cells:
- Left (0,0): already visited with better distance (0 < 2), skip
- Right (0,2): '#' obstacle, skip
Step 3: Process (1,2), distance = 1 (processed first due to appendleft)
- Current cell is 'A', but portal already used (not in dictionary)
- Check adjacent cells:
- Up (0,2): '#' obstacle, skip
- Down (2,2): '.', distance 1+1=2 < β, update
dist[2][2]=2
, append to back βq = [(1,0), (2,2)]
Step 4: Process (1,0), distance = 1
- Current cell is '.', no portal
- Check adjacent cells:
- Up (0,0): already visited with better distance (0 < 2), skip
- Right (1,1): '#' obstacle, skip
- Down (2,0): '.', distance 1+1=2 < β, update
dist[2][0]=2
, append to back βq = [(2,2), (2,0)]
Step 5: Process (2,2), distance = 2
- Destination reached! Return 2
The minimum path: (0,0) β (0,1) β teleport to (1,2) β (2,2) = 2 moves
Notice how the teleportation from (0,1) to (1,2) cost 0 moves, and by using appendleft
, we prioritized exploring from (1,2) before (1,0), allowing us to find the optimal path quickly.
Solution Implementation
1from collections import defaultdict, deque
2from typing import List
3
4class Solution:
5 def minMoves(self, matrix: List[str]) -> int:
6 """
7 Find minimum moves from top-left (0,0) to bottom-right corner.
8 Alphabetic characters act as teleportation points between same letters.
9 '#' represents walls that cannot be traversed.
10 """
11 rows, cols = len(matrix), len(matrix[0])
12
13 # Build a graph of teleportation points
14 # Each letter maps to a list of all positions containing that letter
15 teleport_positions = defaultdict(list)
16 for row_idx, row in enumerate(matrix):
17 for col_idx, cell in enumerate(row):
18 if cell.isalpha():
19 teleport_positions[cell].append((row_idx, col_idx))
20
21 # Direction vectors for moving up, right, down, left
22 directions = (-1, 0, 1, 0, -1)
23
24 # Initialize distance matrix with infinity
25 distance = [[float('inf')] * cols for _ in range(rows)]
26 distance[0][0] = 0
27
28 # BFS queue starting from top-left corner
29 queue = deque([(0, 0)])
30
31 while queue:
32 current_row, current_col = queue.popleft()
33 current_distance = distance[current_row][current_col]
34
35 # Check if we reached the destination
36 if current_row == rows - 1 and current_col == cols - 1:
37 return current_distance
38
39 current_cell = matrix[current_row][current_col]
40
41 # Handle teleportation if current cell is a letter
42 if current_cell in teleport_positions:
43 # Teleport to all positions with the same letter (cost = 0)
44 for teleport_row, teleport_col in teleport_positions[current_cell]:
45 if current_distance < distance[teleport_row][teleport_col]:
46 distance[teleport_row][teleport_col] = current_distance
47 # Add to front of queue since teleportation has no cost
48 queue.appendleft((teleport_row, teleport_col))
49 # Remove this letter from teleportation map to avoid revisiting
50 del teleport_positions[current_cell]
51
52 # Try moving in all four directions
53 for i in range(4):
54 direction_row = directions[i]
55 direction_col = directions[i + 1]
56 next_row = current_row + direction_row
57 next_col = current_col + direction_col
58
59 # Check if the next position is valid and improves the distance
60 if (0 <= next_row < rows and
61 0 <= next_col < cols and
62 matrix[next_row][next_col] != "#" and
63 current_distance + 1 < distance[next_row][next_col]):
64
65 distance[next_row][next_col] = current_distance + 1
66 # Add to back of queue since regular move has cost = 1
67 queue.append((next_row, next_col))
68
69 # Return -1 if destination is unreachable
70 return -1
71
1class Solution {
2 public int minMoves(String[] matrix) {
3 int rows = matrix.length;
4 int cols = matrix[0].length();
5
6 // Map to store positions of each alphabetic character (teleportation points)
7 Map<Character, List<int[]>> teleportMap = new HashMap<>();
8
9 // Build the teleportation map
10 for (int row = 0; row < rows; row++) {
11 String currentRow = matrix[row];
12 for (int col = 0; col < cols; col++) {
13 char cell = currentRow.charAt(col);
14 if (Character.isAlphabetic(cell)) {
15 teleportMap.computeIfAbsent(cell, k -> new ArrayList<>())
16 .add(new int[] {row, col});
17 }
18 }
19 }
20
21 // Direction vectors for moving up, right, down, left
22 int[] directions = {-1, 0, 1, 0, -1};
23
24 // Initialize distance array with infinity
25 int INFINITY = Integer.MAX_VALUE / 2;
26 int[][] distance = new int[rows][cols];
27 for (int[] row : distance) {
28 Arrays.fill(row, INFINITY);
29 }
30
31 // Starting position distance is 0
32 distance[0][0] = 0;
33
34 // Deque for 0-1 BFS (teleportation costs 0, regular moves cost 1)
35 Deque<int[]> queue = new ArrayDeque<>();
36 queue.add(new int[] {0, 0});
37
38 while (!queue.isEmpty()) {
39 int[] current = queue.pollFirst();
40 int currentRow = current[0];
41 int currentCol = current[1];
42 int currentDistance = distance[currentRow][currentCol];
43
44 // Check if we reached the destination
45 if (currentRow == rows - 1 && currentCol == cols - 1) {
46 return currentDistance;
47 }
48
49 // Handle teleportation (0 cost move)
50 char currentCell = matrix[currentRow].charAt(currentCol);
51 if (teleportMap.containsKey(currentCell)) {
52 // Teleport to all positions with the same character
53 for (int[] teleportPosition : teleportMap.get(currentCell)) {
54 int teleportRow = teleportPosition[0];
55 int teleportCol = teleportPosition[1];
56 if (currentDistance < distance[teleportRow][teleportCol]) {
57 distance[teleportRow][teleportCol] = currentDistance;
58 // Add to front since teleportation has 0 cost
59 queue.addFirst(new int[] {teleportRow, teleportCol});
60 }
61 }
62 // Remove processed teleportation points to avoid revisiting
63 teleportMap.remove(currentCell);
64 }
65
66 // Handle regular moves in 4 directions (1 cost move)
67 for (int i = 0; i < 4; i++) {
68 int rowDelta = directions[i];
69 int colDelta = directions[i + 1];
70 int newRow = currentRow + rowDelta;
71 int newCol = currentCol + colDelta;
72
73 // Check bounds, obstacles, and if new distance is better
74 if (newRow >= 0 && newRow < rows &&
75 newCol >= 0 && newCol < cols &&
76 matrix[newRow].charAt(newCol) != '#' &&
77 currentDistance + 1 < distance[newRow][newCol]) {
78
79 distance[newRow][newCol] = currentDistance + 1;
80 // Add to back since regular move has cost 1
81 queue.addLast(new int[] {newRow, newCol});
82 }
83 }
84 }
85
86 // Destination unreachable
87 return -1;
88 }
89}
90
1class Solution {
2public:
3 int minMoves(vector<string>& matrix) {
4 int rows = matrix.size();
5 int cols = matrix[0].size();
6
7 // Map to store positions of each alphabetic character (teleportation points)
8 unordered_map<char, vector<pair<int, int>>> teleportPoints;
9
10 // Build the teleportation points map
11 for (int row = 0; row < rows; ++row) {
12 for (int col = 0; col < cols; ++col) {
13 char cell = matrix[row][col];
14 if (isalpha(cell)) {
15 teleportPoints[cell].push_back({row, col});
16 }
17 }
18 }
19
20 // Direction vectors for moving up, right, down, left
21 int directions[5] = {-1, 0, 1, 0, -1};
22
23 // Initialize distance array with infinity
24 const int INF = numeric_limits<int>::max() / 2;
25 vector<vector<int>> distance(rows, vector<int>(cols, INF));
26 distance[0][0] = 0;
27
28 // 0-1 BFS using deque
29 // Push to front for 0-weight edges (teleportation), back for 1-weight edges (normal move)
30 deque<pair<int, int>> bfsQueue;
31 bfsQueue.push_back({0, 0});
32
33 while (!bfsQueue.empty()) {
34 // Get current position
35 auto [currentRow, currentCol] = bfsQueue.front();
36 bfsQueue.pop_front();
37
38 int currentDist = distance[currentRow][currentCol];
39
40 // Check if we reached the destination
41 if (currentRow == rows - 1 && currentCol == cols - 1) {
42 return currentDist;
43 }
44
45 // Handle teleportation through alphabetic characters
46 char currentCell = matrix[currentRow][currentCol];
47 if (teleportPoints.count(currentCell)) {
48 // Teleport to all other positions with the same character (cost 0)
49 for (auto [targetRow, targetCol] : teleportPoints[currentCell]) {
50 if (currentDist < distance[targetRow][targetCol]) {
51 distance[targetRow][targetCol] = currentDist;
52 bfsQueue.push_front({targetRow, targetCol});
53 }
54 }
55 // Remove this character from map to avoid revisiting
56 teleportPoints.erase(currentCell);
57 }
58
59 // Try moving to adjacent cells (cost 1)
60 for (int dir = 0; dir < 4; ++dir) {
61 int nextRow = currentRow + directions[dir];
62 int nextCol = currentCol + directions[dir + 1];
63
64 // Check boundaries, obstacles, and if new distance is better
65 if (nextRow >= 0 && nextRow < rows &&
66 nextCol >= 0 && nextCol < cols &&
67 matrix[nextRow][nextCol] != '#' &&
68 currentDist + 1 < distance[nextRow][nextCol]) {
69
70 distance[nextRow][nextCol] = currentDist + 1;
71 bfsQueue.push_back({nextRow, nextCol});
72 }
73 }
74 }
75
76 // Destination unreachable
77 return -1;
78 }
79};
80
1/**
2 * Finds the minimum number of moves to reach from top-left to bottom-right in a matrix
3 * with teleportation capability between cells with the same letter
4 * @param matrix - 2D grid where '#' represents walls and letters represent teleportation points
5 * @returns Minimum number of moves to reach destination, or -1 if impossible
6 */
7function minMoves(matrix: string[]): number {
8 const rows = matrix.length;
9 const cols = matrix[0].length;
10
11 // Map to store all positions for each letter (teleportation points)
12 const letterPositions = new Map<string, [number, number][]>();
13
14 // Collect all letter positions in the matrix
15 for (let row = 0; row < rows; row++) {
16 for (let col = 0; col < cols; col++) {
17 const cell = matrix[row][col];
18 // Check if cell contains a letter (uppercase or lowercase)
19 if (/^[A-Za-z]$/.test(cell)) {
20 if (!letterPositions.has(cell)) {
21 letterPositions.set(cell, []);
22 }
23 letterPositions.get(cell)!.push([row, col]);
24 }
25 }
26 }
27
28 // Direction vectors for moving up, right, down, left
29 const directions = [-1, 0, 1, 0, -1];
30
31 // Initialize distance matrix with infinity
32 const INFINITY = Number.MAX_SAFE_INTEGER;
33 const distances: number[][] = Array.from(
34 { length: rows },
35 () => Array(cols).fill(INFINITY)
36 );
37 distances[0][0] = 0;
38
39 // Custom deque implementation using array with two pointers
40 const dequeCapacity = rows * cols * 2 + 5;
41 const deque = new Array<[number, number]>(dequeCapacity);
42 let leftPointer = dequeCapacity >> 1; // Start from middle
43 let rightPointer = dequeCapacity >> 1;
44
45 // Add element to front of deque (for 0-weight edges)
46 const addToFront = (element: [number, number]) => {
47 deque[--leftPointer] = element;
48 };
49
50 // Add element to back of deque (for 1-weight edges)
51 const addToBack = (element: [number, number]) => {
52 deque[rightPointer++] = element;
53 };
54
55 // Remove and return element from front of deque
56 const removeFromFront = (): [number, number] => deque[leftPointer++];
57
58 // Check if deque is empty
59 const isEmpty = () => leftPointer === rightPointer;
60
61 // Start BFS from top-left corner
62 addToBack([0, 0]);
63
64 // 0-1 BFS algorithm
65 while (!isEmpty()) {
66 const [currentRow, currentCol] = removeFromFront();
67 const currentDistance = distances[currentRow][currentCol];
68
69 // Check if we reached the destination
70 if (currentRow === rows - 1 && currentCol === cols - 1) {
71 return currentDistance;
72 }
73
74 // Handle teleportation: if current cell has a letter, teleport to all other cells with same letter
75 const currentCell = matrix[currentRow][currentCol];
76 if (letterPositions.has(currentCell)) {
77 // Teleport to all positions with the same letter (0-weight edge)
78 for (const [targetRow, targetCol] of letterPositions.get(currentCell)!) {
79 if (currentDistance < distances[targetRow][targetCol]) {
80 distances[targetRow][targetCol] = currentDistance;
81 addToFront([targetRow, targetCol]);
82 }
83 }
84 // Remove this letter from map to avoid revisiting
85 letterPositions.delete(currentCell);
86 }
87
88 // Try moving to adjacent cells (1-weight edges)
89 for (let dirIndex = 0; dirIndex < 4; dirIndex++) {
90 const newRow = currentRow + directions[dirIndex];
91 const newCol = currentCol + directions[dirIndex + 1];
92
93 // Check if new position is valid and not a wall
94 if (newRow >= 0 && newRow < rows &&
95 newCol >= 0 && newCol < cols &&
96 matrix[newRow][newCol] !== '#' &&
97 currentDistance + 1 < distances[newRow][newCol]) {
98
99 distances[newRow][newCol] = currentDistance + 1;
100 addToBack([newRow, newCol]);
101 }
102 }
103 }
104
105 // Destination unreachable
106 return -1;
107}
108
Time and Space Complexity
Time Complexity: O(m Γ n)
The algorithm uses BFS to traverse the matrix. Each cell can be visited at most twice:
- Once through normal BFS traversal (moving to adjacent cells)
- Once through teleportation when encountering a letter cell
The key optimization is using del g[c]
after processing teleportation points, ensuring each group of letter cells is processed only once. Since we have at most m Γ n
cells and each cell is processed a constant number of times, the overall time complexity is O(m Γ n)
.
Space Complexity: O(m Γ n)
The space usage includes:
dist
matrix:O(m Γ n)
to store distances to each cellg
dictionary:O(m Γ n)
in worst case if all cells contain letters- BFS queue
q
:O(m Γ n)
in worst case when all cells are enqueued - Other variables use constant space
Therefore, the total space complexity is O(m Γ n)
, where m
and n
are the number of rows and columns of the matrix, respectively.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Handling Portal Deletion Correctly
One critical pitfall is forgetting to delete the portal from the dictionary after using it for teleportation. If you don't remove the portal letter from teleport_positions
after using it, the algorithm might repeatedly attempt to teleport using the same portal, leading to infinite loops or incorrect results.
Incorrect approach:
# Missing deletion - portal can be used infinitely if current_cell in teleport_positions: for teleport_row, teleport_col in teleport_positions[current_cell]: if current_distance < distance[teleport_row][teleport_col]: distance[teleport_row][teleport_col] = current_distance queue.appendleft((teleport_row, teleport_col)) # Forgot to delete: del teleport_positions[current_cell]
Solution: Always delete the portal letter from the dictionary immediately after processing all teleportation destinations to ensure it's only used once.
2. Incorrect Queue Order for 0-1 BFS
Another common mistake is adding both teleportation moves (cost 0) and regular moves (cost 1) to the same end of the deque. This breaks the 0-1 BFS property where cells with lower costs should be processed first.
Incorrect approach:
# Wrong - both moves added to the same end queue.append((teleport_row, teleport_col)) # Should be appendleft for cost 0 queue.append((next_row, next_col)) # Correct for cost 1
Solution: Use appendleft()
for teleportation moves (cost 0) and append()
for regular moves (cost 1) to maintain proper processing order.
3. Processing Portals Multiple Times from Different Paths
If you check whether a portal has been used based on the current cell only, you might process the same portal multiple times when reaching it from different paths. This happens when multiple cells with the same letter are reached at different times.
Incorrect approach:
# Checking if current position was visited instead of checking portal availability
visited_portals = set()
if current_cell.isalpha() and (current_row, current_col) not in visited_portals:
# This doesn't prevent using the same portal letter from different positions
visited_portals.add((current_row, current_col))
Solution: Track portal usage by letter (not position) and delete the entire portal group from the dictionary after first use, ensuring the portal letter can only be used once globally.
4. Misunderstanding Portal Mechanics
A subtle pitfall is misunderstanding that stepping on a portal doesn't automatically trigger teleportation - it's optional. Also, you can walk through a portal cell normally without teleporting.
Incorrect approach:
# Forcing teleportation and not allowing normal movement through portal cells if current_cell.isalpha(): # Only process teleportation, skip normal adjacent moves for teleport_row, teleport_col in teleport_positions[current_cell]: # ... continue # Wrong - should also check adjacent cells
Solution: Process both teleportation options AND normal adjacent cell movements when on a portal cell. The algorithm should explore all possibilities to find the optimal path.
Problem: Given a list of tasks and a list of requirements, compute a sequence of tasks that can be performed, such that we complete every task once while satisfying all the requirements.
Which of the following method should we use to solve this problem?
Recommended Readings
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!