Facebook Pixel

864. Shortest Path to Get All Keys

HardBit ManipulationBreadth-First SearchArrayMatrix
Leetcode Link

Problem Description

You are given an m x n grid where each cell contains one of the following characters:

  • '.' represents an empty cell that you can walk through
  • '#' represents a wall that blocks your path
  • '@' represents your starting position
  • Lowercase letters ('a' to 'f') represent keys you can collect
  • Uppercase letters ('A' to 'F') represent locks that require corresponding keys

You start at the '@' position and can move one step at a time in any of the four cardinal directions (up, down, left, right). You cannot walk outside the grid boundaries or walk through walls.

The movement rules are:

  • You can walk over empty cells freely
  • When you walk over a key, you automatically pick it up
  • You cannot walk through a lock unless you have already collected its corresponding key (e.g., you need key 'a' to pass through lock 'A')

The grid contains exactly k keys and k locks, where 1 <= k <= 6. Each key has exactly one corresponding lock, and they use the first k letters of the alphabet. For example, if there are 3 keys, the grid will contain keys 'a', 'b', 'c' and locks 'A', 'B', 'C'.

Your goal is to find the minimum number of moves required to collect all keys in the grid. If it's impossible to collect all keys, return -1.

For example, if you have a grid with keys 'a' and 'b', you need to find the shortest path that collects both keys, which might require backtracking or taking a specific route to unlock certain areas of the grid.

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 adjacent cells (up, down, left, right) are connected by edges. We need to traverse this graph to find the shortest path.

Is it a tree?

  • No: The grid is not a tree structure - it can have cycles and multiple paths between nodes.

Is the problem related to directed acyclic graphs?

  • No: The grid allows movement in all four directions, creating potential cycles, so it's not a DAG.

Is the problem related to shortest paths?

  • Yes: We need to find the minimum number of moves (shortest path) to collect all keys.

Is the graph Weighted?

  • No: Each move from one cell to an adjacent cell costs exactly 1 step. All edges have uniform weight.

BFS

  • Yes: Since we're looking for the shortest path in an unweighted graph, BFS is the appropriate algorithm.

Conclusion: The flowchart correctly leads us to use BFS (Breadth-First Search) for this problem. BFS is optimal for finding shortest paths in unweighted graphs, which perfectly matches our scenario where each move has the same cost and we need to find the minimum number of moves to collect all keys.

The key insight is that this is a shortest path problem on an unweighted graph (the grid), making BFS the ideal choice. The state compression technique used in the solution (tracking which keys have been collected using bit manipulation) is an implementation detail that enhances the BFS to handle the additional constraint of collecting keys and unlocking doors.

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

Intuition

The key insight is recognizing that this isn't just a simple pathfinding problem - we need to track not only our position but also which keys we've collected. This is because the same position can be visited multiple times with different sets of keys, opening up new paths that were previously blocked by locks.

Think about it this way: imagine you're at position (x, y) and you encounter lock 'B'. Without key 'b', you're stuck. But if you come back to the same position (x, y) later after collecting key 'b', you can now pass through. This means the state of our search isn't just "where we are" but "where we are AND what keys we have."

Since we want the shortest path, BFS is natural - it explores all positions reachable in 1 move, then 2 moves, then 3 moves, and so on. The first time we reach a state where we have all keys is guaranteed to be the shortest path.

The clever part is how to represent "what keys we have." Since there are at most 6 keys ('a' through 'f'), we can use a 6-bit number where each bit represents whether we have that key. For example:

  • 000000 means we have no keys
  • 000001 means we have key 'a'
  • 000011 means we have keys 'a' and 'b'
  • 111111 means we have all 6 keys

This bit representation allows us to efficiently check if we have a key (using bitwise AND) and add a new key to our collection (using bitwise OR).

The visited set needs to track triplets (row, column, key_state) because visiting position (2, 3) with keys 'a' and 'b' is different from visiting (2, 3) with just key 'a'. Each represents a unique state in our search space, and we might need to explore both to find the optimal solution.

When we pick up a new key, we update our state by setting the corresponding bit to 1. When we encounter a lock, we check if the corresponding bit is set. If we ever reach a state where all k bits are set (meaning we have all keys), we've found our answer.

Learn more about Breadth-First Search patterns.

Solution Approach

The implementation follows a state-compressed BFS approach. Let's walk through the key components:

1. Initial Setup First, we scan the grid to find:

  • Starting position (si, sj) where grid[i][j] == '@'
  • Total number of keys k by counting all lowercase letters in the grid

2. State Representation Each state in our BFS is a tuple (i, j, state) where:

  • (i, j) is the current position
  • state is a bitmask representing collected keys
    • If we have key 'a', bit 0 is set: state |= (1 << 0)
    • If we have key 'c', bit 2 is set: state |= (1 << 2)
    • All keys collected when state == (1 << k) - 1

3. BFS Queue and Visited Set

  • Initialize queue q with starting state: [(si, sj, 0)] (no keys initially)
  • Initialize visited set vis with {(si, sj, 0)} to avoid revisiting same state

4. BFS Exploration For each state (i, j, state) popped from queue:

Check completion: If state == (1 << k) - 1, we have all keys - return current step count.

Explore four directions: For each adjacent cell (x, y):

  • Boundary check: Ensure 0 <= x < m and 0 <= y < n
  • Wall check: Skip if grid[x][y] == '#'
  • Lock check: If uppercase letter (lock), check if we have the key:
    if c.isupper() and (state & (1 << (ord(c) - ord('A')))) == 0:
        continue  # Don't have the key, can't pass
  • Key collection: If lowercase letter (key), update state:
    if c.islower():
        nxt |= 1 << (ord(c) - ord('a'))  # Add key to our collection

5. State Tracking Only add new state (x, y, nxt) to queue if not visited before:

if (x, y, nxt) not in vis:
    vis.add((x, y, nxt))
    q.append((x, y, nxt))

6. Level-by-Level Processing The BFS processes all states at distance d before moving to distance d+1:

for _ in range(len(q)):  # Process all states at current distance
    # ... process each state
ans += 1  # Increment distance after processing current level

7. Impossible Case If the queue becomes empty without finding all keys, return -1.

The time complexity is O(m * n * 2^k) where we might visit each cell with each possible key combination. The space complexity is also O(m * n * 2^k) for the visited set. Since k <= 6, the 2^k factor is at most 64, making this approach efficient for the given constraints.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate the solution approach:

Grid:
['@', '.', 'a'],
['A', '#', 'b'],
['.', '.', '.']

Initial Setup:

  • Starting position: (0, 0) where '@' is located
  • Total keys k = 2 (keys 'a' and 'b')
  • Target state: 11 in binary (both bits set) = 3 in decimal

BFS Execution:

Step 0: Queue = [(0, 0, state=00)]

  • Start at (0,0) with no keys (state = 00 in binary)

Step 1: Process (0, 0, 00)

  • Try moving to (0,1): Valid, empty cell '.'
  • Try moving to (1,0): It's lock 'A', need key 'a' (bit 0). State=00, bit 0 not set β†’ blocked
  • Add (0,1,00) to queue

Step 2: Process (0, 1, 00)

  • Try moving to (0,0): Already visited with state 00
  • Try moving to (0,2): It's key 'a'! Pick it up β†’ state becomes 01
  • Try moving to (1,1): Wall '#' β†’ blocked
  • Add (0,2,01) to queue

Step 3: Process (0, 2, 01)

  • We now have key 'a' (state=01)
  • Try moving to (0,1): Add (0,1,01) - different from previous visit (0,1,00)
  • Try moving to (1,2): It's key 'b'! Pick it up β†’ state becomes 11
  • Add (0,1,01) and (1,2,11) to queue

Step 4: Process (0, 1, 01)

  • Try moving to (0,0): Add (0,0,01) - we can revisit start with new keys
  • Try moving to (0,2): Already visited with state 01
  • Try moving to (1,1): Wall β†’ blocked
  • Add (0,0,01) to queue

Step 4 (continued): Process (1, 2, 11)

  • State = 11 (binary) = 3 (decimal) = (1 << 2) - 1
  • All keys collected! Return 4 steps

The path taken:

  1. (0,0) β†’ (0,1) [move right]
  2. (0,1) β†’ (0,2) [move right, collect 'a']
  3. (0,2) β†’ (1,2) [move down, collect 'b']

Total moves: 3

Note how the state tracking was crucial - we couldn't pass through lock 'A' at (1,0) initially, but after collecting key 'a', we could have gone through it if needed. The BFS naturally finds the shortest path by exploring all possibilities level by level.

Solution Implementation

1class Solution:
2    def shortestPathAllKeys(self, grid: List[str]) -> int:
3        rows, cols = len(grid), len(grid[0])
4      
5        # Find the starting position marked with '@'
6        start_row, start_col = next(
7            (i, j) for i in range(rows) for j in range(cols) 
8            if grid[i][j] == '@'
9        )
10      
11        # Count total number of keys (lowercase letters)
12        total_keys = sum(cell.islower() for row in grid for cell in row)
13      
14        # Direction vectors for moving up, right, down, left
15        directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]
16      
17        # BFS queue: (row, col, key_state)
18        # key_state uses bits to represent which keys we have
19        queue = deque([(start_row, start_col, 0)])
20      
21        # Set to track visited states: (row, col, key_state)
22        visited = {(start_row, start_col, 0)}
23      
24        steps = 0
25      
26        # BFS to find shortest path
27        while queue:
28            # Process all nodes at current level
29            for _ in range(len(queue)):
30                current_row, current_col, key_state = queue.popleft()
31              
32                # Check if we have collected all keys
33                if key_state == (1 << total_keys) - 1:
34                    return steps
35              
36                # Explore all four directions
37                for delta_row, delta_col in directions:
38                    next_row = current_row + delta_row
39                    next_col = current_col + delta_col
40                    next_key_state = key_state
41                  
42                    # Check if next position is within grid bounds
43                    if 0 <= next_row < rows and 0 <= next_col < cols:
44                        cell = grid[next_row][next_col]
45                      
46                        # Check if cell is a wall or a locked door without key
47                        if cell == '#':
48                            continue
49                        if cell.isupper():
50                            # Check if we have the key for this lock
51                            key_bit = ord(cell) - ord('A')
52                            if (key_state & (1 << key_bit)) == 0:
53                                continue
54                      
55                        # If cell contains a key, add it to our key state
56                        if cell.islower():
57                            key_bit = ord(cell) - ord('a')
58                            next_key_state |= (1 << key_bit)
59                      
60                        # Add unvisited state to queue
61                        if (next_row, next_col, next_key_state) not in visited:
62                            visited.add((next_row, next_col, next_key_state))
63                            queue.append((next_row, next_col, next_key_state))
64          
65            # Increment step count after processing current level
66            steps += 1
67      
68        # No path found that collects all keys
69        return -1
70
1class Solution {
2    // Direction vectors for moving up, right, down, left
3    private int[] directions = {-1, 0, 1, 0, -1};
4
5    public int shortestPathAllKeys(String[] grid) {
6        int rows = grid.length;
7        int cols = grid[0].length();
8        int totalKeys = 0;
9        int startRow = 0, startCol = 0;
10      
11        // Find starting position and count total number of keys
12        for (int i = 0; i < rows; ++i) {
13            for (int j = 0; j < cols; ++j) {
14                char cell = grid[i].charAt(j);
15                if (Character.isLowerCase(cell)) {
16                    // Count the number of keys in the grid
17                    ++totalKeys;
18                } else if (cell == '@') {
19                    // Mark the starting position
20                    startRow = i;
21                    startCol = j;
22                }
23            }
24        }
25      
26        // BFS queue storing [row, col, key_state]
27        Deque<int[]> queue = new ArrayDeque<>();
28        queue.offer(new int[] {startRow, startCol, 0});
29      
30        // 3D visited array: [row][col][key_state]
31        // key_state uses bit mask to represent collected keys
32        boolean[][][] visited = new boolean[rows][cols][1 << totalKeys];
33        visited[startRow][startCol][0] = true;
34      
35        int steps = 0;
36      
37        // BFS to find shortest path
38        while (!queue.isEmpty()) {
39            int levelSize = queue.size();
40          
41            // Process all nodes at current level
42            for (int i = 0; i < levelSize; ++i) {
43                int[] current = queue.poll();
44                int currentRow = current[0];
45                int currentCol = current[1];
46                int keyState = current[2];
47              
48                // Check if all keys have been collected
49                if (keyState == (1 << totalKeys) - 1) {
50                    return steps;
51                }
52              
53                // Explore four directions
54                for (int dir = 0; dir < 4; ++dir) {
55                    int nextRow = currentRow + directions[dir];
56                    int nextCol = currentCol + directions[dir + 1];
57                  
58                    // Check if next position is within bounds
59                    if (nextRow >= 0 && nextRow < rows && nextCol >= 0 && nextCol < cols) {
60                        char nextCell = grid[nextRow].charAt(nextCol);
61                      
62                        // Skip if it's a wall or a locked door without corresponding key
63                        if (nextCell == '#' || 
64                            (Character.isUpperCase(nextCell) && ((keyState >> (nextCell - 'A')) & 1) == 0)) {
65                            continue;
66                        }
67                      
68                        int nextKeyState = keyState;
69                      
70                        // If current cell contains a key, update the key state
71                        if (Character.isLowerCase(nextCell)) {
72                            nextKeyState |= 1 << (nextCell - 'a');
73                        }
74                      
75                        // Add to queue if this state hasn't been visited
76                        if (!visited[nextRow][nextCol][nextKeyState]) {
77                            visited[nextRow][nextCol][nextKeyState] = true;
78                            queue.offer(new int[] {nextRow, nextCol, nextKeyState});
79                        }
80                    }
81                }
82            }
83          
84            // Increment step count after processing current level
85            ++steps;
86        }
87      
88        // No path found to collect all keys
89        return -1;
90    }
91}
92
1class Solution {
2public:
3    // Direction vectors for moving up, right, down, left
4    const static inline vector<int> dirs = {-1, 0, 1, 0, -1};
5
6    int shortestPathAllKeys(vector<string>& grid) {
7        int rows = grid.size();
8        int cols = grid[0].size();
9        int totalKeys = 0;
10        int startRow = 0, startCol = 0;
11      
12        // Find starting position and count total number of keys
13        for (int i = 0; i < rows; ++i) {
14            for (int j = 0; j < cols; ++j) {
15                char cell = grid[i][j];
16                if (islower(cell)) {
17                    // Count lowercase letters (keys)
18                    ++totalKeys;
19                } else if (cell == '@') {
20                    // Mark starting position
21                    startRow = i;
22                    startCol = j;
23                }
24            }
25        }
26      
27        // BFS queue: stores (row, col, key_state)
28        // key_state uses bitmask to represent collected keys
29        queue<tuple<int, int, int>> bfsQueue{{{startRow, startCol, 0}}};
30      
31        // 3D visited array: [row][col][key_state]
32        // Tracks visited cells for each unique key combination
33        vector<vector<vector<bool>>> visited(rows, 
34            vector<vector<bool>>(cols, vector<bool>(1 << totalKeys)));
35        visited[startRow][startCol][0] = true;
36      
37        int steps = 0;
38      
39        // BFS to find shortest path
40        while (!bfsQueue.empty()) {
41            // Process all nodes at current level
42            for (int nodesAtLevel = bfsQueue.size(); nodesAtLevel > 0; --nodesAtLevel) {
43                auto [currentRow, currentCol, keyState] = bfsQueue.front();
44                bfsQueue.pop();
45              
46                // Check if all keys have been collected
47                if (keyState == (1 << totalKeys) - 1) {
48                    return steps;
49                }
50              
51                // Explore all four directions
52                for (int dir = 0; dir < 4; ++dir) {
53                    int nextRow = currentRow + dirs[dir];
54                    int nextCol = currentCol + dirs[dir + 1];
55                  
56                    // Check if next position is within grid boundaries
57                    if (nextRow >= 0 && nextRow < rows && nextCol >= 0 && nextCol < cols) {
58                        char nextCell = grid[nextRow][nextCol];
59                      
60                        // Skip if wall or locked door without corresponding key
61                        if (nextCell == '#' || 
62                            (isupper(nextCell) && (keyState >> (nextCell - 'A') & 1) == 0)) {
63                            continue;
64                        }
65                      
66                        int nextKeyState = keyState;
67                      
68                        // If found a key, update the key state
69                        if (islower(nextCell)) {
70                            nextKeyState |= 1 << (nextCell - 'a');
71                        }
72                      
73                        // Add to queue if this state hasn't been visited
74                        if (!visited[nextRow][nextCol][nextKeyState]) {
75                            visited[nextRow][nextCol][nextKeyState] = true;
76                            bfsQueue.push({nextRow, nextCol, nextKeyState});
77                        }
78                    }
79                }
80            }
81            // Increment step count after processing current level
82            ++steps;
83        }
84      
85        // No path found that collects all keys
86        return -1;
87    }
88};
89
1// Direction vectors for moving up, right, down, left
2const dirs: number[] = [-1, 0, 1, 0, -1];
3
4function shortestPathAllKeys(grid: string[]): number {
5    const rows: number = grid.length;
6    const cols: number = grid[0].length;
7    let totalKeys: number = 0;
8    let startRow: number = 0;
9    let startCol: number = 0;
10  
11    // Find starting position and count total number of keys
12    for (let i = 0; i < rows; i++) {
13        for (let j = 0; j < cols; j++) {
14            const cell: string = grid[i][j];
15            if (cell >= 'a' && cell <= 'f') {
16                // Count lowercase letters (keys)
17                totalKeys++;
18            } else if (cell === '@') {
19                // Mark starting position
20                startRow = i;
21                startCol = j;
22            }
23        }
24    }
25  
26    // BFS queue: stores [row, col, keyState]
27    // keyState uses bitmask to represent collected keys
28    const bfsQueue: [number, number, number][] = [[startRow, startCol, 0]];
29  
30    // 3D visited set: tracks visited cells for each unique key combination
31    // Using Set with string keys for better performance in TypeScript
32    const visited: Set<string> = new Set();
33    const getVisitedKey = (row: number, col: number, keyState: number): string => {
34        return `${row},${col},${keyState}`;
35    };
36    visited.add(getVisitedKey(startRow, startCol, 0));
37  
38    let steps: number = 0;
39  
40    // BFS to find shortest path
41    while (bfsQueue.length > 0) {
42        // Process all nodes at current level
43        const nodesAtLevel: number = bfsQueue.length;
44        for (let i = 0; i < nodesAtLevel; i++) {
45            const [currentRow, currentCol, keyState] = bfsQueue.shift()!;
46          
47            // Check if all keys have been collected
48            if (keyState === (1 << totalKeys) - 1) {
49                return steps;
50            }
51          
52            // Explore all four directions
53            for (let dir = 0; dir < 4; dir++) {
54                const nextRow: number = currentRow + dirs[dir];
55                const nextCol: number = currentCol + dirs[dir + 1];
56              
57                // Check if next position is within grid boundaries
58                if (nextRow >= 0 && nextRow < rows && nextCol >= 0 && nextCol < cols) {
59                    const nextCell: string = grid[nextRow][nextCol];
60                  
61                    // Skip if wall
62                    if (nextCell === '#') {
63                        continue;
64                    }
65                  
66                    // Skip if locked door without corresponding key
67                    if (nextCell >= 'A' && nextCell <= 'F') {
68                        const doorIndex: number = nextCell.charCodeAt(0) - 'A'.charCodeAt(0);
69                        if ((keyState >> doorIndex & 1) === 0) {
70                            continue;
71                        }
72                    }
73                  
74                    let nextKeyState: number = keyState;
75                  
76                    // If found a key, update the key state
77                    if (nextCell >= 'a' && nextCell <= 'f') {
78                        const keyIndex: number = nextCell.charCodeAt(0) - 'a'.charCodeAt(0);
79                        nextKeyState |= 1 << keyIndex;
80                    }
81                  
82                    // Add to queue if this state hasn't been visited
83                    const visitedKey: string = getVisitedKey(nextRow, nextCol, nextKeyState);
84                    if (!visited.has(visitedKey)) {
85                        visited.add(visitedKey);
86                        bfsQueue.push([nextRow, nextCol, nextKeyState]);
87                    }
88                }
89            }
90        }
91        // Increment step count after processing current level
92        steps++;
93    }
94  
95    // No path found that collects all keys
96    return -1;
97}
98

Time and Space Complexity

Time Complexity: O(m Γ— n Γ— 2^k)

The algorithm uses BFS to explore all possible states. Each state consists of:

  • Current position (x, y) - there are m Γ— n possible positions
  • Key collection state - represented as a bitmask with 2^k possible combinations (where k is the number of keys)

In the worst case, we may need to visit each cell with every possible key combination, giving us m Γ— n Γ— 2^k total states to explore. Each state is processed once due to the visited set vis, and for each state, we check 4 directions (constant time). Therefore, the overall time complexity is O(m Γ— n Γ— 2^k).

Space Complexity: O(m Γ— n Γ— 2^k)

The space is dominated by:

  • The vis set which stores all visited states (x, y, state). In the worst case, this can contain up to m Γ— n Γ— 2^k entries
  • The queue q which in the worst case can also hold O(m Γ— n Γ— 2^k) states

Both data structures can grow to the same order of magnitude, so the total space complexity is O(m Γ— n Γ— 2^k).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrectly Treating Position-Only States as Unique

A critical mistake is visiting the same cell multiple times with different key combinations but treating them as duplicate states. For example:

Incorrect approach:

visited = set()
queue = deque([(start_row, start_col, 0)])
visited.add((start_row, start_col))  # Wrong: only tracking position

while queue:
    row, col, keys = queue.popleft()
    # ...
    if (next_row, next_col) not in visited:  # Wrong check
        visited.add((next_row, next_col))

Why this fails: Consider a grid where you need to:

  1. Visit cell (2, 3) to get key 'a'
  2. Return to cell (1, 1) with key 'a' to unlock door 'A'
  3. Pass through (1, 1) again to reach key 'b'

If we only track positions, we'd mark (1, 1) as visited on first pass and never revisit it with new keys, making the solution impossible.

Correct approach:

visited = set()
visited.add((start_row, start_col, 0))  # Include key state

# Later in the loop:
if (next_row, next_col, next_key_state) not in visited:
    visited.add((next_row, next_col, next_key_state))

2. Mishandling the Target State Check

Incorrect timing of completion check:

# Wrong: Checking after popping from queue
current_row, current_col, key_state = queue.popleft()
if key_state == (1 << total_keys) - 1:
    return steps

This works but can be optimized. A better approach checks when adding to the queue:

# Better: Check when collecting a key
if cell.islower():
    key_bit = ord(cell) - ord('a')
    next_key_state |= (1 << key_bit)
    if next_key_state == (1 << total_keys) - 1:
        return steps + 1  # Found all keys

3. Incorrect Bit Manipulation for Keys and Locks

Common mistakes:

# Wrong: Using wrong bit position
if cell.isupper():
    if key_state & (1 << ord(cell)):  # Wrong: should subtract 'A'
        continue

# Wrong: Not preserving existing keys
if cell.islower():
    next_key_state = 1 << (ord(cell) - ord('a'))  # Wrong: overwrites
    # Should be: next_key_state |= 1 << (ord(cell) - ord('a'))

4. Level-Order BFS Implementation Error

Incorrect distance tracking:

# Wrong: Incrementing steps for each node
while queue:
    current = queue.popleft()
    steps += 1  # Wrong: counts each node, not each level

Correct level-order processing:

steps = 0
while queue:
    # Process all nodes at current distance
    for _ in range(len(queue)):
        current = queue.popleft()
        # Process current node
    steps += 1  # Increment once per level

5. Edge Case: Starting Position Contains a Key

While rare, if the starting position '@' is replaced with a key in some variant:

# Handle initial key collection
initial_keys = 0
if grid[start_row][start_col].islower():
    initial_keys = 1 << (ord(grid[start_row][start_col]) - ord('a'))
queue = deque([(start_row, start_col, initial_keys)])

Solution Summary: The key insight is that the same physical location with different key combinations represents different states in the search space. Always track the triple (row, col, key_state) as the complete state, not just the position.

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

Which data structure is used to implement priority queue?


Recommended Readings

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

Load More