Facebook Pixel

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:

  1. 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
  2. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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":

  1. Regular moves to adjacent cells (cost = 1 move)
  2. 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:

  1. 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.

  2. Distance Matrix (dist): A 2D array initialized to infinity that tracks the minimum distance to reach each cell. We set dist[0][0] = 0 as our starting point.

  3. 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:

  1. Dequeue a cell (i, j) and get its current distance d.

  2. Check if destination reached: If i == m-1 and j == n-1, return the distance.

  3. 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 using appendleft()
    • Delete the portal letter from the dictionary (del g[c]) to ensure it's only used once
  4. 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()

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 Evaluator

Example 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)]

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 cell
  • g 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More