864. Shortest Path to Get All Keys
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.
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 keys000001
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)
wheregrid[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 positionstate
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
- If we have key
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
and0 <= 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 EvaluatorExample 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:
- (0,0) β (0,1) [move right]
- (0,1) β (0,2) [move right, collect 'a']
- (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 arem Γ n
possible positions - Key collection state - represented as a bitmask with
2^k
possible combinations (wherek
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 tom Γ n Γ 2^k
entries - The queue
q
which in the worst case can also holdO(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:
- Visit cell (2, 3) to get key 'a'
- Return to cell (1, 1) with key 'a' to unlock door 'A'
- 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.
Which data structure is used to implement priority queue?
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!