πŸš€ Phases 1–4 are live β€” Days 1–13 + Day 17 are fully written. See the roadmap β†’

Keys and Rooms medium

Description

There are n rooms, labeled 0 to n - 1. All rooms are locked except room 0. Each room contains a list of keys, and each key unlocks exactly one other room.

Given rooms β€” an array where rooms[i] is the list of keys found in room i β€” return true if you can visit all the rooms, and false otherwise. You start in room 0.

Examples

> Case 1:
    rooms = [[1], [2], [3], []]
    Output: true
    # Start in 0 β†’ key to 1. Open 1 β†’ key to 2. Open 2 β†’ key to 3. Done.
 
> Case 2:
    rooms = [[1, 3], [3, 0, 1], [2], [0]]
    Output: false
    # Can reach 0, 1, 3 but never 2 (no key to 2 exists anywhere we can reach).

Constraints

  • 2 <= n <= 1000
  • 0 <= rooms[i].length <= 1000
  • Keys are room labels in [0, n).

Recognizing the graph

Each room is a vertex. Each key in room i pointing to room j is a directed edge i β†’ j. The question becomes:

Starting at vertex 0, can we reach every other vertex?

That’s a reachability question β€” exactly what BFS or DFS answers. Visit all vertices reachable from 0, then check if you’ve visited all n.

Approach 1: Iterative DFS with a stack

Time: O(N + K) where N = rooms, K = total keys. Space: O(N) for the visited set + stack.

Approach 2: Recursive DFS

The same logic in recursive form β€” sometimes cleaner:

def can_visit_all_rooms(rooms):
    visited = set()
    def dfs(room):
        visited.add(room)
        for key in rooms[room]:
            if key not in visited:
                dfs(key)
    dfs(0)
    return len(visited) == len(rooms)

Approach 3: BFS

Equivalent β€” just replace the stack with a queue:

from collections import deque
 
def can_visit_all_rooms(rooms):
    visited = {0}
    queue = deque([0])
    while queue:
        room = queue.popleft()
        for key in rooms[room]:
            if key not in visited:
                visited.add(key)
                queue.append(key)
    return len(visited) == len(rooms)

For this problem (just reachability, no shortest-path requirement) DFS and BFS are interchangeable. DFS is slightly cheaper in cache locality; BFS gives you the BFS layer order if you needed it.

Why does the visited set matter? Without it, a graph with cycles (e.g., room 0 has a key to room 1, room 1 has a key to room 0) would loop forever. The visited set is the defining difference between graph traversal and tree traversal β€” we covered this on the DFS page.

Edge cases

  • rooms = [[]] β†’ only one room, trivially visited. Return true. (n=1 not allowed by constraints but the logic handles it.)
  • rooms = [[1], []] β†’ start at 0, get key to 1, visit 1. All visited. true.
  • rooms = [[], [0]] β†’ start at 0 with no keys. Room 1 unreachable. false.
  • Self-loops (room 0 has a key to room 0) are fine β€” they’re filtered by the visited check.

The pattern: β€œcan I reach all nodes from a given start?”

Keys and Rooms is the pure reachability template β€” DFS/BFS from a starting vertex, count visited at the end. The same shape solves:

  • Find if Path Exists β€” reachability between two specific vertices.
  • Number of Connected Components β€” DFS from every unvisited vertex; the number of times you start a new search = the number of components.
  • All Paths from Source to Target β€” like reachability but record paths.
  • Possible Bipartition β€” reachability with two-coloring.
  • Is the Graph Connected? β€” DFS from any one vertex and check all are visited.

This is also the simplest possible graph algorithm pattern β€” and the gateway to everything heavier on Day 10.

Analysis

  • Time: O(N + K) β€” every room visited once, every key examined at most once.
  • Space: O(N) β€” visited set holds at most all rooms.