latency.lab
Algorithms · Topic 11

Graphs

Level: foundationTool: BFS/DFS, topological sort, union-find, DijkstraTime: ~80 min

By the end you can

Graphs generalize trees: instead of a single parent per node, any node can connect to any other, in any pattern — cycles included. That extra freedom is exactly why this is the topic interviews lean on most: almost any "how are these things related/reachable/orderable" question, however it's dressed up (a grid, a word list, a course catalog, a network), is a graph problem wearing a costume.

The two traversals you already know, generalized

BFS and DFS aren't new here — Trees already used both. The difference is that a graph node can be reached more than one way, so every traversal needs an explicit visited set (a tree never revisits a node by construction; a graph absolutely can, including right back to where you started). Number of Islands and Flood Fill are the clearest versions of this: BFS outward from a cell, marking each one visited the moment you enqueue it, so a cycle in the grid's connectivity never re-processes the same cell twice.

A B C D E F
dequeued & processed
in queue (discovered)
undiscovered
One step into BFS from A: A is dequeued, and all three of its neighbors — B and F via the outer ring, D via the dashed shortcut edge — are enqueued at once. C and E are still undiscovered. Without a visited set, processing D's neighbors later would try to re-enqueue A right back onto the queue.
The habit to build

On a large, densely-connected input, prefer a queue-based BFS over recursive DFS. A single connected component can span the entire input, and a recursive DFS recurses once per node in it — on a large enough graph, that's a real stack overflow, not just a style preference (Number of Islands' large single-island test exists specifically to catch this).

Topological sort: ordering with constraints

Course Schedule asks "is there a valid order at all" (does the dependency graph have a cycle); Course Schedule II asks for the order itself. Both use the same algorithm — Kahn's algorithm: track each node's in-degree (how many prerequisites point at it), seed a queue with every in-degree-0 node, and repeatedly pop one, "complete" it, and decrement its neighbors' in-degrees, pushing any that just hit 0. If every node eventually gets popped, there's no cycle; anything left over is stuck in one. The queue is what keeps this O(V + E) — rescanning every node each round for one with in-degree 0, instead of remembering which ones just became available, turns this into O(V²) for no reason.

Union-Find: is this the same component as that?

Number of Connected Components, Graph Valid Tree, and Redundant Connection all ask a version of "are these two nodes already connected" — repeatedly, as edges are added one at a time. A Disjoint Set Union structure answers that in near-O(1) per query, but only with two specific optimizations working together: path compression (every find flattens the path it just walked, so future lookups on those nodes are instant) and union by rank (always attach the shorter tree under the taller one, keeping trees shallow to begin with). Drop either one, and a specific adversarial sequence of unions can build a tree of depth n — turning "near O(1)" into genuinely O(n) per lookup.

union-find-both-optimizations.cpp
int find(int x) {
    if (parent[x] != x) parent[x] = find(parent[x]); // path compression
    return parent[x];
}
bool unite(int a, int b) {
    int ra = find(a), rb = find(b);
    if (ra == rb) return false; // already connected
    if (rank_[ra] < rank_[rb]) std::swap(ra, rb); // union by rank:
    parent[rb] = ra;                          // attach the SHORTER tree under the taller one
    if (rank_[ra] == rank_[rb]) rank_[ra]++;
    return true;
}
A B C D E
before: find(E) walks E→D→C→B→A
⇒
A B C D E
after: every node on that path now points directly to A
Path compression in action. The chain A←B←C←D←E (each node's parent pointer aimed at the one above it) took 4 hops to resolve find(E). Once compressed, find on B, C, D, or E is a single hop — but only union by rank stops a chain this tall from forming in the first place on the next sequence of unions.

BFS finds shortest paths — for free, in unweighted graphs

Word Ladder and Rotting Oranges look unrelated (a dictionary vs. a grid) but are the same idea: BFS naturally explores in order of distance from the start, so the first time it reaches any node is necessarily via a shortest path. Word Ladder's efficiency trick is in how it generates neighbors — trying all 26 possible letters at each position (a fixed 26·L candidates) instead of comparing every pair of words in the dictionary (which is O(N²) for no reason). Rotting Oranges' trick is starting the BFS from every rotten orange at once — a "multi-source" BFS — so all of them spread in parallel in one pass, rather than running a separate BFS per source and combining the results afterward.

When edges have weights: Dijkstra

Network Delay Time is the one problem here where "fewest edges" isn't the same as "least total cost" — a 2-hop path can beat a 1-hop path if the 1-hop edge is expensive enough. Dijkstra's algorithm is BFS's natural generalization for this: instead of a plain queue, a min-heap keyed by current best-known distance always expands the closest unfinished node next. The heap is what keeps this O(E log V); without it, finding "the closest unfinished node" each round means scanning all V of them, which is O(V²) — the same recompute-vs-maintain distinction that shows up throughout this course.

Common mistakes

  • Forgetting the visited set — the single most common graph bug; without it, a cycle in the input turns a traversal into an infinite loop instead of just revisiting a node harmlessly.
  • Recursive DFS on a graph that might be huge and densely connected — correct in theory, but a real stack-overflow risk in practice; BFS with an explicit queue sidesteps this entirely.
  • Union-Find with only one of the two optimizations — "it has path compression, that's enough" (or vice versa for rank) is a common half-truth; either one alone still allows a bad case, just a different one.
  • Treating a weighted shortest-path problem like an unweighted one — plain BFS assumes every edge costs the same "1 step"; the moment edges have different weights, only Dijkstra (or another weighted-shortest-path algorithm) gives a correct answer.

Takeaways

  • Graph traversal is tree traversal plus an explicit visited set — the thing a tree gets for free by construction, a graph must track by hand.
  • Kahn's algorithm turns "does a valid order exist" and "what is that order" into the same O(V + E) queue-driven process.
  • Union-Find answers "are these already connected" in near-O(1) per query, but only with both path compression AND union by rank — either alone still has a bad case.
  • BFS finds shortest paths for free in unweighted graphs; the moment edges have weights, that guarantee breaks and Dijkstra's min-heap-driven expansion takes over.

Try it: BFS with an explicit visited set

Number of Islands is the cleanest version of graph BFS — get the visited-marking and queue-based traversal automatic here, since every other problem in this topic builds on it.

Live

Loading starter code…

Try it: topological sort

Course Schedule — Kahn's algorithm, detecting a cycle by tracking in-degrees and a queue of what's currently available.

Live

Loading starter code…

Try it: Union-Find

Number of Connected Components — get both path compression and union by rank right here; the large adversarial-chain test is specifically designed to catch a version with only one of them.

Live

Loading starter code…

Keep going

That's 3 of the 12 problems in this topic. Word Ladder and Network Delay Time are worth doing next — BFS-as-shortest-path and its weighted generalization (Dijkstra), the two ends of this topic's arc. See the full Graphs set →

Checkpoint · Graphs

5 questions · pass at 70%

Q01
Number of Islands uses a queue-based BFS instead of a recursive DFS for the flood-fill step. What's the actual reason, beyond style preference?
WhyBoth BFS and DFS are O(rows*cols) here -- the real difference is call stack depth. A recursive DFS's stack depth equals the size of the largest connected island, which can be the entire grid; a queue-based BFS never recurses at all, so it has no stack-depth risk regardless of how large a single island gets.
Q02
Course Schedule II's Kahn's-algorithm solution maintains a queue of currently-available (indegree-0) courses. What specifically goes wrong if you instead rescan ALL courses every round looking for one with indegree 0?
WhyThe queue is exactly the memory of 'which courses are available right now' -- maintaining it incrementally as indegrees hit zero is what keeps each course's availability check O(1) amortized. Rescanning everything every round recomputes that same fact V times over, turning a linear pass into a quadratic one.
Q03
Union-Find's two standard optimizations are path compression and union by rank. Why does using only ONE of them still leave a bad case, rather than being 'good enough'?
WhyUnion by rank keeps trees shallow as they're built, but by itself doesn't retroactively shorten a path once it's been walked. Path compression shortens paths as they're walked, but without union by rank, nothing stops an adversarial union order from building a tall tree in the first place. Together, neither failure mode is exploitable.
Q04
Word Ladder generates each word's neighbors by trying all 26 letters at each position, rather than comparing every pair of words in the dictionary to see which differ by one letter. Why does this matter?
WhyAll-pairs comparison scales with the SQUARE of the dictionary size, since every word gets compared against every other word. Pattern-based generation scales linearly in dictionary size (each word only ever generates its own 26*L candidates and checks each in a hash set) -- the gap widens fast as the dictionary grows.
Q05
Network Delay Time needs Dijkstra's algorithm rather than plain BFS. What specifically breaks if you use plain BFS on a graph with weighted edges?
WhyBFS's guarantee ("first time reached = shortest path") relies entirely on every edge costing the same one step -- that's what makes level-by-level exploration equivalent to distance order. The moment edges have different weights, a node reached via more hops but lower total weight can be genuinely closer, which plain BFS has no way to account for. Dijkstra's min-heap-by-distance is exactly the fix.

Finished Graphs?

Pass the quiz to complete it automatically.