Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Breadth-First Search (BFS)

Builds on Level-Order BFS, which you haven’t finished yet.

P2EasyBuilt on Level-Order BFS~5 min speedrun

BFS on a Graph (Mark on Push)

Built on
Level-Order BFSA seen set marks a node when it is added to queue, so a node with two parents is expanded once and a cycle ends.
Grows into
Shortest-Path BFS
The problem

Find if Path Exists in Graph (LC 1971)(opens leetcode.com)

A graph has n nodes, numbered from 0 to n - 1. The list edges describes its connections. Each pair [u, v] is one edge, and it joins node u and node v in both directions, so you can walk it either way.

You start at node source. Return true if you can walk along edges to node destination, and false if you cannot.

Example 1
Input:n = 3, edges = [[0,1],[1,2],[2,0]], source = 0, destination = 2
Output:true
Two paths lead from node 0 to node 2. One is 0 -> 1 -> 2. The other is 0 -> 2, which uses the edge [2, 0] in the direction from 0 to 2.
012

Graph BFS

graph with cycles ➔ mark `seen` when pushed ➔ each node queued once
Spot it when: The input is a graph given as edges or adjacency lists, it can have cycles, and you must reach, find or visit nodes starting from one node.
Graph BFS
Python
1from collections import defaultdict, deque
2from typing import Dict, List
3 
4def validPath(n: int, edges: List[List[int]], source: int, destination: int) -> bool:
5 """Find if Path Exists in Graph (LC 1971): BFS from source that marks a node in seen when it is added to queue.
6 V = n nodes. E = len(edges). The graph is undirected and can have cycles.
7 Time: O(V + E) | Space: O(V + E) for the adjacency lists, seen and queue
8 """
9 if source == destination:
10 return True
11 
12 graph: Dict[int, List[int]] = defaultdict(list) # node -> its neighbours, built from the edge list
13 for u, v in edges:
14 graph[u].append(v)
15 graph[v].append(u)
16 
17 seen = {source} # every node that is in queue or has already left it
18 queue = deque([source]) # nodes found but not yet expanded, in the order they were found
19 
20 while queue:
21 # 1. Expand the Oldest Found Node:
22 # Every neighbour of node is looked at once, when node leaves queue.
23 node = queue.popleft()
24 for neighbour in graph[node]:
25 if neighbour == destination:
26 return True
27 
28 # 2. Mark on Push:
29 # A neighbour joins seen at the moment it joins queue, not when it leaves.
30 # A node that waits in queue is then already marked, so a second parent
31 # cannot add it again, and a cycle cannot refill the queue.
32 if neighbour not in seen:
33 seen.add(neighbour)
34 queue.append(neighbour)
35 
36 return False
Invariant: Every node in seen is in queue or has already left it, so each node is added to queue exactly once.

A graph can reach one node from two parents, so mark a node in seen at the moment you add it to queue, not when you take it out. Each node then enters queue once, so a cycle cannot refill the queue and the loop ends. The trap is skipping the seen check: on a cycle, the loop never ends when destination cannot be reached.

See it run

Mark on push, step by step

seen [0] · destination 6

seen [0] · destination 6

seen [0, 1] · destination 6

seen [0, 1, 2] · destination 6

seen [0, 1, 2] · destination 6

seen [0, 1, 2] · destination 6

seen [0, 1, 2, 3] · destination 6

seen [0, 1, 2, 3] · destination 6

seen [0, 1, 2, 3] · destination 6

seen [0, 1, 2, 3] · destination 6

seen [0, 1, 2, 3] · destination 6

seen [0, 1, 2, 3] · destination 6

seen [0, 1, 2, 3] · destination 6

seen [0, 1, 2, 3, 4] · destination 6

seen [0, 1, 2, 3, 4] · destination 6

seen [0, 1, 2, 3, 4] · destination 6

seen [0, 1, 2, 3, 4] · destination 6 · answer False

Each circle is a node. The first row under the graph is queue: the left box leaves first. The second row is seen.

  • node the node just taken out of queue
  • queued waiting in queue, already in seen
  • expanded all its neighbours were looked at
  • neighbour the neighbour being looked at now

Put the source 0 in queue and in seen. The destination is 6.

Take 0 out of queue. Look at its neighbours [1, 2] one by one.

Neighbour 1 is not in seen. Add it to seen and to queue together.

Neighbour 2 is not in seen. Add it to seen and to queue together.

Take 1 out of queue. Look at its neighbours [0, 3] one by one.

Neighbour 0 is already in seen, so it is not added to queue again.

Neighbour 3 is not in seen. Add it to seen and to queue together.

Take 2 out of queue. Look at its neighbours [0, 3] one by one.

Neighbour 0 is already in seen, so it is not added to queue again.

Neighbour 3 is already in seen, so it is not added to queue again.

Take 3 out of queue. Look at its neighbours [1, 2, 4] one by one.

Neighbour 1 is already in seen, so it is not added to queue again.

Neighbour 2 is already in seen, so it is not added to queue again.

Neighbour 4 is not in seen. Add it to seen and to queue together.

Take 4 out of queue. Look at its neighbours [3] one by one.

Neighbour 3 is already in seen, so it is not added to queue again.

queue is empty and 6 was never reached, so the answer is False.

0queued123456
queue
0
seen
0
1 / 17
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Adding a neighbour without checking seen

    Mistake
    queue.append(neighbour)
    Fix
    if neighbour not in seen:
        seen.add(neighbour)
        queue.append(neighbour)

    What happens: A graph can have a cycle. Without the seen check, every neighbour goes into queue again and again. On a cycle the nodes keep refilling queue, so the loop never ends when the destination cannot be reached.

    Example: n = 4, edges = [[0,1],[1,2],[2,0]], source = 0, destination = 3
    Mistake: the loop never endsFix: returns False
  2. 2

    Gotcha 2: Adding only one direction of each edge

    Mistake
    graph[u].append(v)
    Fix
    graph[u].append(v)
    graph[v].append(u)

    What happens: An edge [u, v] joins both nodes. Adding v to the list of u only lets the search walk from u to v. A search that starts at v finds no neighbour.

    Example: n = 2, edges = [[0,1]], source = 1, destination = 0
    Mistake: returns FalseFix: returns True
  3. 3

    Gotcha 3: Skipping the check source == destination

    Mistake
    pass
    Fix
    if source == destination:
        return True

    What happens: The search only compares a neighbour with destination. The source itself is never a neighbour of anything in this walk, so a path of length 0 is never found. The graph can even have no edges.

    Example: n = 1, edges = [], source = 0, destination = 0
    Mistake: returns FalseFix: returns True
  4. 4

    Gotcha 4: Marking a node when it leaves queue instead of when it joins

    Mistake
    node = queue.popleft()
    seen.add(node)
    Fix
    seen.add(neighbour)
    queue.append(neighbour)

    What happens: A node that waits in queue is not yet in seen. A second parent finds it unmarked and adds it again. The answer stays correct. The code adds a node to seen only when it takes the node out of queue, and skips a node that is already in seen. Each parent still adds the node to queue before that. On a complete graph with 10 nodes, the queue receives 45 pushes instead of 9.

Not this one if…
  • The input is a tree, as in Binary Tree Level Order Traversal (LC 102): no node has two parents, so no seen set is needed. See Level-Order BFS
  • You need the fewest moves, as in Shortest Path in Binary Matrix (LC 1091), not only whether a node can be reached: each node also stores dist. See Shortest-Path BFS

Deep Dive

Why a graph needs seen and a tree does not

In a tree, every node has one parent. The walk reaches each node by exactly one path, so no node is added to queue twice.

In a graph, a node can have two parents or sit on a cycle. In the board, node 3 is a neighbour of both 1 and 2. The walk reaches it from node 1 first. When it later looks at node 3 from node 2, node 3 is already in seen, so it is not added again.

The board uses source = 0 and destination = 6. Node 6 is in a separate group of nodes, so queue empties and the answer is False. seen ends as [0, 1, 2, 3, 4], and each of those nodes was added to queue once.

Mark on push: why the mark goes before queue.append

seen.add(neighbour) runs in the same step as queue.append(neighbour). The invariant is: every node in queue, and every node that has already left queue, is in seen.

If the mark waits until the node leaves queue, a node that is still waiting is not marked. A second parent then adds it again. The answer stays correct, but the same node is expanded many times.

On a complete graph with 10 nodes, marking on push puts 9 nodes into queue. Marking on pop means the code adds a node to seen only when it takes the node out of queue, skips a node that is already in seen, and adds a neighbour to queue unless the neighbour is already in seen. Then queue gets 45 entries, one for each pair of nodes.

What happens with no seen at all

Take the three links [0, 1], [1, 2] and [2, 0], with source = 0 and destination = 3. The three nodes form a cycle. Node 3 has no link.

Without seen, node 0 adds 1 and 2. Node 1 adds 0 and 2. Node 2 adds 1 and 0. Each of them adds the others again, so queue never empties. The loop never ends.

With seen, the three nodes are added once each, queue empties after three nodes, and the answer is False.

The cost, and what the card does not add

The code builds the adjacency lists in O(V + E) time. Each node enters queue once and leaves it once. Each link is looked at twice, once from each end. So the search takes O(V + E) time, and graph, seen and queue take O(V + E) space.

This card answers the question: can the destination be reached? It does not store how far each node is. The card Shortest-Path BFS adds a dist value to each node for that.

For a start with many sources, or a graph that is a grid, the same seen rule holds. Only the way the neighbours are listed changes.

60-Second Pitch

"The graph is undirected and can have cycles, so I build adjacency lists and run BFS from the source. I keep a seen set and add a node to seen at the same moment I add it to queue. A node that is waiting in queue is then already marked, so a second parent cannot add it again, and a cycle cannot refill the queue. If a neighbour is the destination, I return True. If queue empties first, I return False. Each node enters queue once and each link is looked at twice, so the time is O(V + E) and the space is O(V + E)."

Common Questions

Why do you mark a node when you add it to queue and not when you take it out?

A node that is waiting in queue is not yet marked if you mark on pop. A second parent then adds it again. The answer stays correct, but a node can be expanded once for each link that points to it. On a complete graph with 10 nodes that is 45 queue entries instead of 9.

Can you use depth-first search instead? What changes?

Yes, for this question. Replace queue.popleft() with stack.pop(), or use recursion. The seen rule is the same. The answer to "is there a path" does not depend on the order. BFS is needed when the number of moves matters.

What are the time and space complexity?

O(V + E) time and O(V + E) space. Building the adjacency lists costs O(V + E). Each node enters queue once. Each link is looked at twice, once from each end.

What if the input is an adjacency matrix and not a list of edges?

Then finding the neighbours of a node costs a scan of one row, which is O(V). The search takes O(V^2) time. The seen rule does not change.

How would you also count the nodes that can be reached?

Return len(seen) when queue is empty. seen holds exactly the nodes that were added to queue, which are the nodes reachable from the source.

SWE Reasoning

Spot it

  • The input is a graph: a list of edges, an adjacency list or an adjacency matrix. It can have cycles.
  • The question is about reaching: is there a path, how many nodes are reachable, visit every node once, copy the graph.
  • No weights and no order of visiting is required, so a plain queue is enough.
  • A node can be reached from two different parents. This is the signal that a seen set is needed.

The invariant, step by step

  1. seen starts as {source} and queue starts as [source]. Both hold the source, so the invariant holds.
  2. Each round takes the oldest node out of queue and looks at its neighbours.
  3. A neighbour that is the destination ends the search with True.
  4. A neighbour that is not in seen is added to seen and to queue in the same step. The invariant still holds.
  5. A neighbour already in seen is left alone. It is in queue or already expanded.
  6. When queue is empty, every node reachable from the source was expanded, so the answer is False.

Traps

  • No seen check. On a cycle, queue never empties when the destination cannot be reached.
  • Only one direction of each edge. The graph is undirected, so both graph[u] and graph[v] need the link.
  • No check for source == destination. The walk compares only neighbours, so a path of length 0 is missed.
  • Marking when the node leaves queue. A waiting node is not marked, so it is added again and expanded many times.

Practice

Canonical LeetCode:Find if Path Exists in Graph (LC 1971)Clone Graph (LC 133)Keys and Rooms (LC 841)Number of Provinces (LC 547)
Theory Context•Breadth-First Search (BFS)

Builds on Level-Order BFS, which you haven’t finished yet.

P2EasyBuilt on Level-Order BFS~5 min speedrun

BFS on a Graph (Mark on Push)

Built on
Level-Order BFSA seen set marks a node when it is added to queue, so a node with two parents is expanded once and a cycle ends.
Grows into
Shortest-Path BFS
The problem

Find if Path Exists in Graph (LC 1971)(opens leetcode.com)

A graph has n nodes, numbered from 0 to n - 1. The list edges describes its connections. Each pair [u, v] is one edge, and it joins node u and node v in both directions, so you can walk it either way.

You start at node source. Return true if you can walk along edges to node destination, and false if you cannot.

Example 1
Input:n = 3, edges = [[0,1],[1,2],[2,0]], source = 0, destination = 2
Output:true
Two paths lead from node 0 to node 2. One is 0 -> 1 -> 2. The other is 0 -> 2, which uses the edge [2, 0] in the direction from 0 to 2.
012

Graph BFS

graph with cycles ➔ mark `seen` when pushed ➔ each node queued once
Spot it when: The input is a graph given as edges or adjacency lists, it can have cycles, and you must reach, find or visit nodes starting from one node.
Graph BFS
Python
1from collections import defaultdict, deque
2from typing import Dict, List
3 
4def validPath(n: int, edges: List[List[int]], source: int, destination: int) -> bool:
5 """Find if Path Exists in Graph (LC 1971): BFS from source that marks a node in seen when it is added to queue.
6 V = n nodes. E = len(edges). The graph is undirected and can have cycles.
7 Time: O(V + E) | Space: O(V + E) for the adjacency lists, seen and queue
8 """
9 if source == destination:
10 return True
11 
12 graph: Dict[int, List[int]] = defaultdict(list) # node -> its neighbours, built from the edge list
13 for u, v in edges:
14 graph[u].append(v)
15 graph[v].append(u)
16 
17 seen = {source} # every node that is in queue or has already left it
18 queue = deque([source]) # nodes found but not yet expanded, in the order they were found
19 
20 while queue:
21 # 1. Expand the Oldest Found Node:
22 # Every neighbour of node is looked at once, when node leaves queue.
23 node = queue.popleft()
24 for neighbour in graph[node]:
25 if neighbour == destination:
26 return True
27 
28 # 2. Mark on Push:
29 # A neighbour joins seen at the moment it joins queue, not when it leaves.
30 # A node that waits in queue is then already marked, so a second parent
31 # cannot add it again, and a cycle cannot refill the queue.
32 if neighbour not in seen:
33 seen.add(neighbour)
34 queue.append(neighbour)
35 
36 return False
Invariant: Every node in seen is in queue or has already left it, so each node is added to queue exactly once.

A graph can reach one node from two parents, so mark a node in seen at the moment you add it to queue, not when you take it out. Each node then enters queue once, so a cycle cannot refill the queue and the loop ends. The trap is skipping the seen check: on a cycle, the loop never ends when destination cannot be reached.

See it run

Mark on push, step by step

seen [0] · destination 6

seen [0] · destination 6

seen [0, 1] · destination 6

seen [0, 1, 2] · destination 6

seen [0, 1, 2] · destination 6

seen [0, 1, 2] · destination 6

seen [0, 1, 2, 3] · destination 6

seen [0, 1, 2, 3] · destination 6

seen [0, 1, 2, 3] · destination 6

seen [0, 1, 2, 3] · destination 6

seen [0, 1, 2, 3] · destination 6

seen [0, 1, 2, 3] · destination 6

seen [0, 1, 2, 3] · destination 6

seen [0, 1, 2, 3, 4] · destination 6

seen [0, 1, 2, 3, 4] · destination 6

seen [0, 1, 2, 3, 4] · destination 6

seen [0, 1, 2, 3, 4] · destination 6 · answer False

Each circle is a node. The first row under the graph is queue: the left box leaves first. The second row is seen.

  • node the node just taken out of queue
  • queued waiting in queue, already in seen
  • expanded all its neighbours were looked at
  • neighbour the neighbour being looked at now

Put the source 0 in queue and in seen. The destination is 6.

Take 0 out of queue. Look at its neighbours [1, 2] one by one.

Neighbour 1 is not in seen. Add it to seen and to queue together.

Neighbour 2 is not in seen. Add it to seen and to queue together.

Take 1 out of queue. Look at its neighbours [0, 3] one by one.

Neighbour 0 is already in seen, so it is not added to queue again.

Neighbour 3 is not in seen. Add it to seen and to queue together.

Take 2 out of queue. Look at its neighbours [0, 3] one by one.

Neighbour 0 is already in seen, so it is not added to queue again.

Neighbour 3 is already in seen, so it is not added to queue again.

Take 3 out of queue. Look at its neighbours [1, 2, 4] one by one.

Neighbour 1 is already in seen, so it is not added to queue again.

Neighbour 2 is already in seen, so it is not added to queue again.

Neighbour 4 is not in seen. Add it to seen and to queue together.

Take 4 out of queue. Look at its neighbours [3] one by one.

Neighbour 3 is already in seen, so it is not added to queue again.

queue is empty and 6 was never reached, so the answer is False.

0queued123456
queue
0
seen
0
1 / 17
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Adding a neighbour without checking seen

    Mistake
    queue.append(neighbour)
    Fix
    if neighbour not in seen:
        seen.add(neighbour)
        queue.append(neighbour)

    What happens: A graph can have a cycle. Without the seen check, every neighbour goes into queue again and again. On a cycle the nodes keep refilling queue, so the loop never ends when the destination cannot be reached.

    Example: n = 4, edges = [[0,1],[1,2],[2,0]], source = 0, destination = 3
    Mistake: the loop never endsFix: returns False
  2. 2

    Gotcha 2: Adding only one direction of each edge

    Mistake
    graph[u].append(v)
    Fix
    graph[u].append(v)
    graph[v].append(u)

    What happens: An edge [u, v] joins both nodes. Adding v to the list of u only lets the search walk from u to v. A search that starts at v finds no neighbour.

    Example: n = 2, edges = [[0,1]], source = 1, destination = 0
    Mistake: returns FalseFix: returns True
  3. 3

    Gotcha 3: Skipping the check source == destination

    Mistake
    pass
    Fix
    if source == destination:
        return True

    What happens: The search only compares a neighbour with destination. The source itself is never a neighbour of anything in this walk, so a path of length 0 is never found. The graph can even have no edges.

    Example: n = 1, edges = [], source = 0, destination = 0
    Mistake: returns FalseFix: returns True
  4. 4

    Gotcha 4: Marking a node when it leaves queue instead of when it joins

    Mistake
    node = queue.popleft()
    seen.add(node)
    Fix
    seen.add(neighbour)
    queue.append(neighbour)

    What happens: A node that waits in queue is not yet in seen. A second parent finds it unmarked and adds it again. The answer stays correct. The code adds a node to seen only when it takes the node out of queue, and skips a node that is already in seen. Each parent still adds the node to queue before that. On a complete graph with 10 nodes, the queue receives 45 pushes instead of 9.

Not this one if…
  • The input is a tree, as in Binary Tree Level Order Traversal (LC 102): no node has two parents, so no seen set is needed. See Level-Order BFS
  • You need the fewest moves, as in Shortest Path in Binary Matrix (LC 1091), not only whether a node can be reached: each node also stores dist. See Shortest-Path BFS

Deep Dive

Why a graph needs seen and a tree does not

In a tree, every node has one parent. The walk reaches each node by exactly one path, so no node is added to queue twice.

In a graph, a node can have two parents or sit on a cycle. In the board, node 3 is a neighbour of both 1 and 2. The walk reaches it from node 1 first. When it later looks at node 3 from node 2, node 3 is already in seen, so it is not added again.

The board uses source = 0 and destination = 6. Node 6 is in a separate group of nodes, so queue empties and the answer is False. seen ends as [0, 1, 2, 3, 4], and each of those nodes was added to queue once.

Mark on push: why the mark goes before queue.append

seen.add(neighbour) runs in the same step as queue.append(neighbour). The invariant is: every node in queue, and every node that has already left queue, is in seen.

If the mark waits until the node leaves queue, a node that is still waiting is not marked. A second parent then adds it again. The answer stays correct, but the same node is expanded many times.

On a complete graph with 10 nodes, marking on push puts 9 nodes into queue. Marking on pop means the code adds a node to seen only when it takes the node out of queue, skips a node that is already in seen, and adds a neighbour to queue unless the neighbour is already in seen. Then queue gets 45 entries, one for each pair of nodes.

What happens with no seen at all

Take the three links [0, 1], [1, 2] and [2, 0], with source = 0 and destination = 3. The three nodes form a cycle. Node 3 has no link.

Without seen, node 0 adds 1 and 2. Node 1 adds 0 and 2. Node 2 adds 1 and 0. Each of them adds the others again, so queue never empties. The loop never ends.

With seen, the three nodes are added once each, queue empties after three nodes, and the answer is False.

The cost, and what the card does not add

The code builds the adjacency lists in O(V + E) time. Each node enters queue once and leaves it once. Each link is looked at twice, once from each end. So the search takes O(V + E) time, and graph, seen and queue take O(V + E) space.

This card answers the question: can the destination be reached? It does not store how far each node is. The card Shortest-Path BFS adds a dist value to each node for that.

For a start with many sources, or a graph that is a grid, the same seen rule holds. Only the way the neighbours are listed changes.

60-Second Pitch

"The graph is undirected and can have cycles, so I build adjacency lists and run BFS from the source. I keep a seen set and add a node to seen at the same moment I add it to queue. A node that is waiting in queue is then already marked, so a second parent cannot add it again, and a cycle cannot refill the queue. If a neighbour is the destination, I return True. If queue empties first, I return False. Each node enters queue once and each link is looked at twice, so the time is O(V + E) and the space is O(V + E)."

Common Questions

Why do you mark a node when you add it to queue and not when you take it out?

A node that is waiting in queue is not yet marked if you mark on pop. A second parent then adds it again. The answer stays correct, but a node can be expanded once for each link that points to it. On a complete graph with 10 nodes that is 45 queue entries instead of 9.

Can you use depth-first search instead? What changes?

Yes, for this question. Replace queue.popleft() with stack.pop(), or use recursion. The seen rule is the same. The answer to "is there a path" does not depend on the order. BFS is needed when the number of moves matters.

What are the time and space complexity?

O(V + E) time and O(V + E) space. Building the adjacency lists costs O(V + E). Each node enters queue once. Each link is looked at twice, once from each end.

What if the input is an adjacency matrix and not a list of edges?

Then finding the neighbours of a node costs a scan of one row, which is O(V). The search takes O(V^2) time. The seen rule does not change.

How would you also count the nodes that can be reached?

Return len(seen) when queue is empty. seen holds exactly the nodes that were added to queue, which are the nodes reachable from the source.

SWE Reasoning

Spot it

  • The input is a graph: a list of edges, an adjacency list or an adjacency matrix. It can have cycles.
  • The question is about reaching: is there a path, how many nodes are reachable, visit every node once, copy the graph.
  • No weights and no order of visiting is required, so a plain queue is enough.
  • A node can be reached from two different parents. This is the signal that a seen set is needed.

The invariant, step by step

  1. seen starts as {source} and queue starts as [source]. Both hold the source, so the invariant holds.
  2. Each round takes the oldest node out of queue and looks at its neighbours.
  3. A neighbour that is the destination ends the search with True.
  4. A neighbour that is not in seen is added to seen and to queue in the same step. The invariant still holds.
  5. A neighbour already in seen is left alone. It is in queue or already expanded.
  6. When queue is empty, every node reachable from the source was expanded, so the answer is False.

Traps

  • No seen check. On a cycle, queue never empties when the destination cannot be reached.
  • Only one direction of each edge. The graph is undirected, so both graph[u] and graph[v] need the link.
  • No check for source == destination. The walk compares only neighbours, so a path of length 0 is missed.
  • Marking when the node leaves queue. A waiting node is not marked, so it is added again and expanded many times.

Practice

Canonical LeetCode:Find if Path Exists in Graph (LC 1971)Clone Graph (LC 133)Keys and Rooms (LC 841)Number of Provinces (LC 547)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•