DSA sheet · Graphs · BFS pattern · single-source reachability

Keys and Rooms

A story about locked rooms and keys that turns out to be the simplest graph question there is: start from room 0 and check whether a traversal can reach every room. The teacher spots that the input is already an adjacency list (the keys in a room are the rooms you can go to next), explains BFS on the two examples, gives two ways to decide "did I visit everything?" (scan the visited array, or count the rooms that came out of the queue), and then codes the DFS version. It's a good warm-up for spotting "this is just reachability" inside a wordy problem.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Graph basics this page needs

Nodes and edges

A graph is a set of nodes (points) joined by edges (lines). Here every room is a node, and "room a holds the key to room b" is an edge from a to b: once you're in a, you can go to b.

Directed, undirected, weighted

kindmeaningdrawn as
undirectedWorks both ways.(0)—(1)
directedOne way only. This problem is directed: the key to room 1 lying in room 0 lets you go 0 → 1. It says nothing about going from 1 back to 0.(0)→(1)
weightedA number on each edge. No weights here.(0)—5—(1)

Three ways the input can arrive, and the adjacency list

formlooks like (for 0 → 1 → 2 → 3)where can I go from u?
edge list[[0,1],[1,2],[2,3]]scan all edges, so convert first
adjacency matrix4 × 4 grid, M[u][v] = 1 for u → vevery v with M[u][v] == 1
adjacency list[[1],[2],[3],[]]adj[u]

Look at the last row: that's exactly the input format of this problem. rooms[u] is the list of keys in room u, i.e. the rooms you can go to from u. The teacher's point: normally the first job in a graph problem is to build an adjacency list so we know a node's "children"; here rooms already is one, so we skip that step. For other problems, this is how you'd build one from an edge list:

edge list → adjacency list (directed)
n = 4
edges = [[0, 1], [1, 2], [2, 3]]
adj = [[] for _ in range(n)]
for u, v in edges:
    adj[u].append(v)           # directed: only u -> v
print(adj)                     # [[1], [2], [3], []]

The visited array

Rooms can hold keys to rooms we've already seen, even to themselves or back to room 0 (Example 2 does this). Without a record of "already been here", we'd keep walking the same rooms in a loop. So we keep visited, one True/False per room, and only enter a room that is not visited yet.

Single source, and connected components

Usually we loop over every node and start a new traversal from each unvisited one, because the graph may be in several pieces (connected components). Here the problem fixes the start: only room 0 is open. That makes it a single-source question: one traversal from 0, then ask "did it reach everyone?" If some room sits in a piece that 0 can't reach, the answer is False.

BFS and DFS

BFS: Breadth First SearchDFS: Depth First Search
ideaLevel by level: room 0, then every room whose key is in room 0, then the next layer…Use one key, go into that room, use its first key, go deeper… come back when stuck.
toolcollections.deque as a queuerecursion (the call stack)
reachesexactly the same set of rooms. Only the order differs, and the order doesn't matter here.

Part A · BFS from room 0

LeetCode 841 · Keys and Rooms

1The question in simple words

There are n rooms, numbered 0 to n−1. All are locked except room 0. Inside each room is a set of keys; key k opens room k. You can pick up every key you find and use it later. rooms[i] lists the keys in room i. Return True if you can get into every room, otherwise False.

Example 1: rooms = [[1],[2],[3],[]]
(0)→(1)→(2)→(3)

room 0 has key 1, room 1 has key 2,
room 2 has key 3, room 3 is empty
all 4 rooms reachable → True
Example 2: rooms = [[1,3],[3,0,1],[2],[0]]
(0) ⇄ (1)↺       0→1 0→3
 ⇅   ↙           1→3 1→0 1→1 (its own key)
(3)              3→0

(2)↺             key 2 is only inside room 2
room 2 never opens → False

2What the constraints tell us

3Intuition: a treasure hunt

Stand in room 0 with an empty bag. Pick up every key you see. Keep a list of "rooms I can open but haven't explored yet". Explore them one by one, picking up more keys. When the list runs out, look back: did you get into every room? The teacher's summary: the only task is to visit all the rooms we can, so it's just a DFS or BFS traversal that marks rooms visited.

4Building the logic from the teacher's examples

Example 1, with a queue

Put room 0 in the queue. Pop 0: it has key 1, so add room 1. Pop 1: key 2, add room 2. Pop 2: key 3, add room 3. Pop 3: no keys. The queue is empty and we went into all 4 rooms → True.

Only add rooms we haven't seen

Mark a room visited the moment we add it to the queue, and only add rooms that are not visited. Two good things follow: no room goes into the queue twice, and loops like "room 1 holds the key to room 0" can't trap us.

Two ways to answer "did I visit everything?"

  1. Scan the visited array. After the BFS, if any room is still False, we never got in → return False. Otherwise True.
  2. Count. Because each room enters the queue at most once, the number of rooms popped from the queue equals the number of different rooms we entered. Keep count += 1 on every pop, and at the end compare count == len(rooms).
Doubt 1: why is the count trick allowed?
→ Only because we never push a room twice. If we pushed without checking visited, room 0 could be popped three times in Example 2, and the count would be wrong. The visited check is what makes "pops = distinct rooms" true.

Example 2, and the teacher's slip

While walking through Example 2 she first misreads one of the lists and finds that every room opens, then notices the mismatch and redoes it with the real input. The correct walk: pop 0 → keys 1 and 3, both new, push them. Pop 1 → keys 3, 0, 1, all already visited, push nothing. Pop 3 → key 0, visited. The queue is empty. We popped 3 rooms but there are 4. Room 2 never opened, because its only key is locked inside room 2 itself → False.

5Approach steps

  1. visited = [False] * n; mark room 0 and push it.
  2. While the queue isn't empty: pop a room, add 1 to count.
  3. For each key in that room: if the room it opens isn't visited, mark it and push it.
  4. Return count == n.

6Code (Python)

Part A · BFS from room 0, counting the rooms entered
from collections import deque

class Solution:
    def canVisitAllRooms(self, rooms):
        n = len(rooms)
        visited = [False] * n
        visited[0] = True                 # room 0 is open
        q = deque([0])
        count = 0
        while q:
            room = q.popleft()
            count += 1                    # one more distinct room entered
            for key in rooms[room]:
                if not visited[key]:
                    visited[key] = True
                    q.append(key)
        return count == n                 # or: return all(visited)

7Code line by line

linewhat it means
visited = [False] * nOne flag per room. No adjacency list needed: rooms already is one.
visited[0] = True q = deque([0])The single source: we start inside room 0.
room = q.popleft() count += 1Enter the next room in line and count it. Each room is popped at most once.
for key in rooms[room]:The keys here are this node's neighbours (the rooms we can go to next).
if not visited[key]: visited[key] = True q.append(key)A room we haven't reached yet: mark it now (when pushing) so it never gets pushed again.
return count == nEntered as many distinct rooms as exist → all of them. all(visited) gives the same answer (way 1).

8Dry run

Example 2: rooms = [[1,3],[3,0,1],[2],[0]]

steproom poppedkeys checkedqueue aftervisited after [0,1,2,3]count
0start-[0][T, F, F, F]0
101 new ✓ push · 3 new ✓ push[1, 3][T, T, F, T]1
213 ✗ visited · 0 ✗ · 1 ✗[3][T, T, F, T]2
330 ✗ visited[ ][T, T, F, T]3
4queue empty3 == 4? no--False
start0room 0 is open
pop 013picked up keys 1 and 3
pop 13keys 3, 0, 1: all old
pop 3emptykey 0: old → done, count = 3

Example 1: rooms = [[1],[2],[3],[]]

start0count 0
pop 01count 1
pop 12count 2
pop 23count 3
pop 3emptycount 4 == 4 → True

9Complexity & remember

Doubt 2: the video calls the time "n + n". Is that exact?
→ It's n for the rooms plus the work of reading key lists. The key-reading part is really the total number of keys K, which can be more than n: if every room held n keys, reading them all would be n × n even though almost all are skipped as visited. LeetCode caps K at 3000, so in practice it's tiny. The safe way to write it is O(n + K), the usual "nodes + edges" of a graph traversal.
Remember Part AKeys are edges, rooms is the adjacency list. BFS from 0, mark on push, count pops. count == n → True.

Part B · DFS from room 0

After explaining BFS, the teacher codes the DFS version, saying that anyone following the playlist should be able to write both for a pure traversal problem. Steps ①–④ are the same as Part A. What changes:

Doubt: why sys.setrecursionlimit?
→ In a chain like 0 → 1 → 2 → … → 999, the DFS goes 1000 calls deep before it can return. Python's default limit is about 1000 frames (and a few are already in use), so it would crash with RecursionError. Raising the limit fixes it; the BFS version never has this issue.

5Approach steps

  1. visited = [False] * n; call dfs(0).
  2. dfs(room): mark it True; for each key, if that room isn't visited, call dfs(key).
  3. After the call: if any entry of visited is False → False, else True.

6Code (Python)

Part B · DFS from room 0 (the teacher's approach)
import sys

class Solution:
    def canVisitAllRooms(self, rooms):
        sys.setrecursionlimit(10000)
        visited = [False] * len(rooms)

        def dfs(room):
            visited[room] = True
            for key in rooms[room]:
                if not visited[key]:
                    dfs(key)

        dfs(0)
        for v in visited:
            if not v:
                return False              # a room we never got into
        return True

7Code line by line

linewhat it means
visited[room] = TrueFirst thing on entering a room: mark it.
for key in rooms[room]:Look at every key found here, one by one.
if not visited[key]: dfs(key)Only go into rooms we haven't entered yet. That's what stops loops like 1 → 0 → 1.
dfs(0)Single source: everything starts from room 0.
for v in visited: if not v: return FalseAny room still False was never reached.
return TrueNo False found: every room was entered.

8Dry run

Example 2: rooms = [[1,3],[3,0,1],[2],[0]]

steproom visitedkeys checkedcall stack aftervisited after
1dfs(0)key 1: new → dfs(1)dfs(0) dfs(1)[T, T, F, F]
21key 3: new → dfs(3)dfs(0) dfs(1) dfs(3)[T, T, F, T]
33key 0: visited ✗ → returndfs(0) dfs(1)same
4back in 1keys 0, 1: visited ✗ → returndfs(0)same
5back in 0key 3: visited ✗ → return(empty)[T, T, F, T]
6scan visitedindex 2 is False-False
Example 2, deepest (step 2)
dfs(0)dfs(1)dfs(3)
Example 1, deepest
dfs(0)dfs(1)dfs(2)dfs(3)

In Example 1 the calls go straight down 0 → 1 → 2 → 3, every room gets marked, the scan finds no False → True.

9Complexity & remember

Remember Part Bdfs(0) fills visited; then any False → False. Raise the recursion limit in Python.

Part C · Revision page

BFS (Part A)DFS (Part B)
graphdirected; rooms[u] is already the adjacency list
startroom 0 only (single source, no outer loop)
pending rooms kept indequecall stack
"all visited?" checkcount pops, count == nscan visited for a False
time / spaceO(n + K) / O(n)O(n + K) / O(n)
If you remember only 5 lines 1. Rooms are nodes, keys are directed edges.
2. rooms is already an adjacency list: don't build one.
3. Traverse (BFS or DFS) from room 0, marking visited.
4. All visited → True; any room left → False.
5. Counting pops works only because each room is pushed once.
Mistakes to avoid ✗ treating keys as two-way (key to 1 in room 0 does not open 0 from 1)
✗ pushing rooms without the visited check (loops, wrong count)
✗ forgetting to mark room 0 visited at the start
✗ a recursive DFS on 1000 chained rooms without raising the limit
✗ looping over every room as a start (only room 0 is open)
test it yourself (paste under any of the solutions above)
s = Solution()
print(s.canVisitAllRooms([[1], [2], [3], []]))                 # True
print(s.canVisitAllRooms([[1, 3], [3, 0, 1], [2], [0]]))       # False
print(s.canVisitAllRooms([[1], []]))                           # True
print(s.canVisitAllRooms([[], [0]]))                           # False
print(s.canVisitAllRooms([[1, 2, 3], [], [], []]))             # True

Based on this video: Keys and Rooms | Graph | Pattern-wise DSA sheet