841. Keys and Rooms
Problem Description
You are given n
rooms numbered from 0
to n - 1
. Initially, all rooms are locked except room 0
, which you can enter freely.
Each room contains a set of keys. When you visit a room, you collect all the keys in that room. Each key is labeled with a number indicating which room it can unlock. For example, if you find a key labeled 3
in a room, you can use it to unlock and enter room 3
.
The input rooms
is an array where rooms[i]
represents the list of keys you can find in room i
. Your task is to determine whether it's possible to visit all n
rooms starting from room 0
.
How the solution works:
The solution uses Depth-First Search (DFS) to explore all reachable rooms:
- Start from room
0
(the only unlocked room initially) - When visiting a room, mark it as visited using a set
vis
- Collect all keys in the current room and recursively visit each room that these keys unlock
- If a room has already been visited, skip it to avoid infinite loops
- After the DFS completes, check if the number of visited rooms equals the total number of rooms
The function returns True
if all rooms can be visited (i.e., len(vis) == len(rooms)
), and False
otherwise.
Example walkthrough:
If rooms = [[1], [2], [3], []]
:
- Start in room
0
, find key1
- Visit room
1
, find key2
- Visit room
2
, find key3
- Visit room
3
, find no keys - All 4 rooms visited β return
True
If rooms = [[1, 3], [3, 0, 1], [2], [0]]
:
- Start in room
0
, find keys1
and3
- Visit room
1
, find keys3
,0
,1
(already have these) - Visit room
3
, find key0
(already have) - Cannot reach room
2
β returnFalse
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 problem involves rooms (nodes) and keys that unlock other rooms (edges). Room
i
has keys that point to other rooms, forming a directed graph structure.
Is it a tree?
- No: The graph can have cycles (a room can have a key back to a previously visited room) and multiple paths to the same room. It's not necessarily hierarchical like a tree.
Is the problem related to directed acyclic graphs?
- No: The graph can contain cycles. For example, room 1 might have a key to room 2, and room 2 might have a key back to room 1.
Is the problem related to shortest paths?
- No: We don't need to find the shortest path to any room. We only need to determine if all rooms are reachable from room 0.
Does the problem involve connectivity?
- Yes: The core question is about connectivity - can we reach all rooms starting from room 0? We need to explore which rooms are connected (reachable) from the starting room.
Does the problem have small constraints?
- Yes: The constraints are typically small (n β€ 3000 rooms), making DFS/backtracking feasible without worrying about stack overflow or time limits.
DFS/backtracking?
- Yes: DFS is perfect for exploring all reachable rooms from room 0. We recursively visit each room, collect its keys, and explore the rooms those keys unlock.
Conclusion: The flowchart suggests using DFS (Depth-First Search) to explore the graph of rooms and determine if all rooms are reachable from the starting room 0.
Intuition
Think of this problem like exploring a building where you start in the lobby (room 0) and need to determine if you can visit every room. Each room contains keys to other rooms, and once you pick up a key, you can use it anytime.
The key insight is that this is a reachability problem in a directed graph. Each room is a node, and having a key to a room creates a directed edge to that room. Starting from room 0, we need to explore all rooms we can reach by following these edges.
Why DFS works perfectly here:
-
Natural exploration pattern: When you enter a room, you immediately want to explore all the rooms whose keys you just found. This matches DFS's behavior of going as deep as possible before backtracking.
-
No need to track paths: We don't care about the order or path we take to visit rooms - we only care about whether we can eventually reach all rooms. This makes DFS simpler than algorithms that track distances or paths.
-
Avoiding revisits: Once we've visited a room and collected its keys, there's no benefit to visiting it again. We use a
visited
set to mark rooms we've already explored, preventing infinite loops in case of cycles (like room A having a key to room B, and room B having a key back to room A).
The solution naturally emerges: Start a DFS from room 0, mark each visited room, and recursively explore all rooms whose keys we find. After the DFS completes, if the number of visited rooms equals the total number of rooms, we know all rooms are reachable. If some rooms remain unvisited, it means they're disconnected from room 0's reachable component - we can never get keys to those rooms no matter how we explore.
Learn more about Depth-First Search, Breadth-First Search and Graph patterns.
Solution Approach
The implementation uses Depth-First Search (DFS) with a recursive approach and a set to track visited rooms.
Data Structures Used:
vis
: A set to store visited room numbers. Using a set gives us O(1) lookup time to check if a room has been visited.
Algorithm Steps:
-
Initialize the visited set: Create an empty set
vis
to track which rooms we've explored. -
Define the DFS function:
def dfs(i: int): if i in vis: return vis.add(i) for j in rooms[i]: dfs(j)
- Takes a room number
i
as input - First checks if room
i
has already been visited - if yes, return immediately (base case) - Mark room
i
as visited by adding it tovis
- Iterate through all keys found in
rooms[i]
and recursively visit each unlocked room
- Takes a room number
-
Start DFS from room 0: Call
dfs(0)
since room 0 is the only initially unlocked room. -
Check if all rooms are visited: Compare
len(vis)
withlen(rooms)
:- If equal: All rooms were reachable, return
True
- If less: Some rooms couldn't be reached, return
False
- If equal: All rooms were reachable, return
Why this works:
- The DFS explores all rooms reachable from room 0 by following available keys
- The visited set prevents infinite loops and redundant exploration
- After DFS completes,
vis
contains exactly the set of reachable rooms - If this set's size equals the total number of rooms, we've successfully visited all rooms
Time Complexity: O(N + K) where N is the number of rooms and K is the total number of keys across all rooms. We visit each room at most once and examine each key once.
Space Complexity: O(N) for the visited set and the recursion call stack in the worst case.
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 with rooms = [[1,2], [3], [], [2]]
to illustrate the DFS solution approach.
Initial Setup:
- We have 4 rooms (0, 1, 2, 3)
- Room 0 contains keys [1, 2]
- Room 1 contains key [3]
- Room 2 contains no keys []
- Room 3 contains key [2]
- Only room 0 is initially unlocked
vis
= {} (empty set)
Step-by-step DFS execution:
Step 1: Call dfs(0)
- Check: Is 0 in
vis
? No, so continue - Add 0 to
vis
:vis
= {0} - Room 0 has keys [1, 2], so we'll call
dfs(1)
anddfs(2)
Step 2: Call dfs(1)
(first key from room 0)
- Check: Is 1 in
vis
? No, so continue - Add 1 to
vis
:vis
= {0, 1} - Room 1 has key [3], so call
dfs(3)
Step 3: Call dfs(3)
(key from room 1)
- Check: Is 3 in
vis
? No, so continue - Add 3 to
vis
:vis
= {0, 1, 3} - Room 3 has key [2], so call
dfs(2)
Step 4: Call dfs(2)
(key from room 3)
- Check: Is 2 in
vis
? No, so continue - Add 2 to
vis
:vis
= {0, 1, 2, 3} - Room 2 has no keys, so this branch ends
- Return to Step 3
Step 5: Back in Step 3, done with room 3
- Return to Step 2
Step 6: Back in Step 2, done with room 1
- Return to Step 1
Step 7: Back in Step 1, now call dfs(2)
(second key from room 0)
- Check: Is 2 in
vis
? Yes (added in Step 4), so return immediately - Back to Step 1
Step 8: DFS complete
- Check:
len(vis)
= 4,len(rooms)
= 4 - Since 4 == 4, return
True
All rooms were successfully visited! The DFS explored the path 0β1β3β2, and when it tried to revisit room 2 from room 0's second key, it correctly skipped it since it was already visited.
Solution Implementation
1class Solution:
2 def canVisitAllRooms(self, rooms: List[List[int]]) -> bool:
3 """
4 Determines if all rooms can be visited starting from room 0.
5 Each room contains keys to other rooms.
6
7 Args:
8 rooms: List where rooms[i] contains keys to other rooms
9
10 Returns:
11 True if all rooms can be visited, False otherwise
12 """
13 def depth_first_search(room_index: int) -> None:
14 """
15 Performs DFS traversal to visit rooms and collect keys.
16
17 Args:
18 room_index: Current room number to visit
19 """
20 # Skip if room already visited
21 if room_index in visited_rooms:
22 return
23
24 # Mark current room as visited
25 visited_rooms.add(room_index)
26
27 # Visit all rooms for which we have keys from current room
28 for key in rooms[room_index]:
29 depth_first_search(key)
30
31 # Set to track visited rooms
32 visited_rooms = set()
33
34 # Start DFS from room 0 (initially unlocked)
35 depth_first_search(0)
36
37 # Check if all rooms have been visited
38 return len(visited_rooms) == len(rooms)
39
1class Solution {
2 // Counter to track the number of rooms visited
3 private int visitedRoomCount;
4
5 // Boolean array to mark which rooms have been visited
6 private boolean[] visited;
7
8 // Adjacency list representation of rooms and their keys
9 private List<List<Integer>> roomGraph;
10
11 /**
12 * Determines if all rooms can be visited starting from room 0.
13 * Each room contains keys to other rooms represented as a list.
14 *
15 * @param rooms List where rooms[i] contains keys to other rooms
16 * @return true if all rooms can be visited, false otherwise
17 */
18 public boolean canVisitAllRooms(List<List<Integer>> rooms) {
19 // Initialize the room graph with the input
20 roomGraph = rooms;
21
22 // Initialize visited array with size equal to number of rooms
23 visited = new boolean[roomGraph.size()];
24
25 // Start DFS traversal from room 0 (which is always unlocked)
26 dfs(0);
27
28 // Check if all rooms have been visited
29 return visitedRoomCount == roomGraph.size();
30 }
31
32 /**
33 * Performs depth-first search to visit rooms.
34 *
35 * @param roomIndex The current room index to visit
36 */
37 private void dfs(int roomIndex) {
38 // If room has already been visited, return to avoid cycles
39 if (visited[roomIndex]) {
40 return;
41 }
42
43 // Mark current room as visited
44 visited[roomIndex] = true;
45
46 // Increment the count of visited rooms
47 ++visitedRoomCount;
48
49 // Visit all rooms for which we have keys from current room
50 for (int nextRoom : roomGraph.get(roomIndex)) {
51 dfs(nextRoom);
52 }
53 }
54}
55
1class Solution {
2public:
3 bool canVisitAllRooms(vector<vector<int>>& rooms) {
4 int totalRooms = rooms.size();
5 int visitedCount = 0;
6
7 // Track which rooms have been visited
8 vector<bool> visited(totalRooms, false);
9
10 // Depth-first search to explore all reachable rooms
11 function<void(int)> dfs = [&](int currentRoom) {
12 // Skip if already visited
13 if (visited[currentRoom]) {
14 return;
15 }
16
17 // Mark current room as visited
18 visited[currentRoom] = true;
19 visitedCount++;
20
21 // Visit all rooms accessible from current room's keys
22 for (int nextRoom : rooms[currentRoom]) {
23 dfs(nextRoom);
24 }
25 };
26
27 // Start exploring from room 0 (which is initially unlocked)
28 dfs(0);
29
30 // Check if all rooms were visited
31 return visitedCount == totalRooms;
32 }
33};
34
1/**
2 * Determines if all rooms can be visited starting from room 0
3 * @param rooms - Array where rooms[i] contains keys to other rooms
4 * @returns true if all rooms can be visited, false otherwise
5 */
6function canVisitAllRooms(rooms: number[][]): boolean {
7 const totalRooms: number = rooms.length;
8 const visitedRooms: boolean[] = Array(totalRooms).fill(false);
9
10 /**
11 * Depth-first search to explore all reachable rooms
12 * @param currentRoom - The current room index being visited
13 */
14 const exploreRoom = (currentRoom: number): void => {
15 // Skip if room has already been visited
16 if (visitedRooms[currentRoom]) {
17 return;
18 }
19
20 // Mark current room as visited
21 visitedRooms[currentRoom] = true;
22
23 // Visit all rooms accessible from current room using available keys
24 for (const nextRoom of rooms[currentRoom]) {
25 exploreRoom(nextRoom);
26 }
27 };
28
29 // Start exploration from room 0 (which is initially unlocked)
30 exploreRoom(0);
31
32 // Check if all rooms have been visited
33 return visitedRooms.every((isVisited: boolean) => isVisited);
34}
35
Time and Space Complexity
The time complexity is O(n + m)
, where n
is the number of rooms and m
is the total number of keys across all rooms (edges in the graph). The DFS algorithm visits each room at most once due to the visited set check, contributing O(n)
. Additionally, we iterate through all keys in the rooms we visit, contributing O(m)
. Since we explore each edge (key) exactly once when we visit a room, the total time complexity is O(n + m)
.
The space complexity is O(n)
, where n
is the number of rooms. This accounts for:
- The visited set
vis
which can store at mostn
room indices:O(n)
- The recursive call stack depth in the worst case (when rooms form a linear chain):
O(n)
Therefore, the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Handling Empty Input or Edge Cases
A common mistake is not considering edge cases like when rooms
is empty or contains only one room.
Pitfall Example:
def canVisitAllRooms(self, rooms: List[List[int]]) -> bool:
vis = set()
def dfs(i):
vis.add(i) # Might throw error if rooms is empty
for j in rooms[i]:
if j not in vis:
dfs(j)
dfs(0) # Assumes room 0 exists
return len(vis) == len(rooms)
Solution:
def canVisitAllRooms(self, rooms: List[List[int]]) -> bool:
if not rooms: # Handle empty input
return True
vis = set()
def dfs(i):
if i in vis:
return
vis.add(i)
for j in rooms[i]:
dfs(j)
dfs(0)
return len(vis) == len(rooms)
2. Forgetting to Check Visited Status Before Recursion
Without checking if a room is already visited before making the recursive call, the algorithm may enter infinite loops or have poor performance.
Pitfall Example:
def canVisitAllRooms(self, rooms: List[List[int]]) -> bool:
vis = set()
def dfs(i):
vis.add(i)
for j in rooms[i]:
dfs(j) # No check - will revisit rooms indefinitely
dfs(0)
return len(vis) == len(rooms)
Solution:
def canVisitAllRooms(self, rooms: List[List[int]]) -> bool:
vis = set()
def dfs(i):
if i in vis: # Check before processing
return
vis.add(i)
for j in rooms[i]:
dfs(j) # Now safe to recurse
dfs(0)
return len(vis) == len(rooms)
3. Using a List Instead of a Set for Tracking Visited Rooms
Using a list for vis
leads to O(n) lookup time when checking if a room has been visited, significantly degrading performance.
Pitfall Example:
def canVisitAllRooms(self, rooms: List[List[int]]) -> bool:
vis = [] # List has O(n) lookup time
def dfs(i):
if i in vis: # This is O(n) operation
return
vis.append(i)
for j in rooms[i]:
dfs(j)
dfs(0)
return len(vis) == len(rooms)
Solution:
def canVisitAllRooms(self, rooms: List[List[int]]) -> bool:
vis = set() # Set has O(1) lookup time
def dfs(i):
if i in vis: # This is O(1) operation
return
vis.add(i)
for j in rooms[i]:
dfs(j)
dfs(0)
return len(vis) == len(rooms)
4. Stack Overflow with Deep Recursion
For inputs with many rooms in a linear chain, recursive DFS might cause stack overflow. Consider using iterative DFS for production code.
Pitfall Example:
# Recursive approach might fail with deep chains
def canVisitAllRooms(self, rooms: List[List[int]]) -> bool:
vis = set()
def dfs(i):
if i in vis:
return
vis.add(i)
for j in rooms[i]:
dfs(j) # Deep recursion might overflow
dfs(0)
return len(vis) == len(rooms)
Solution (Iterative Approach):
def canVisitAllRooms(self, rooms: List[List[int]]) -> bool:
vis = set([0]) # Start with room 0 visited
stack = [0] # Use explicit stack instead of recursion
while stack:
current_room = stack.pop()
for key in rooms[current_room]:
if key not in vis:
vis.add(key)
stack.append(key)
return len(vis) == len(rooms)
Consider the classic dynamic programming of longest increasing subsequence:
Find the length of the longest subsequence of a given sequence such that all elements of the subsequence are sorted in increasing order.
For example, the length of LIS for [50, 3, 10, 7, 40, 80]
is 4
and LIS is
[3, 7, 40, 80]
.
What is the recurrence relation?
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
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
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Want a Structured Path to Master System Design Too? Donβt Miss This!