LC Patterns — Graphs

Previous: binary-search, two-pointers, sliding-window, 1d-dp, arrays-strings, trees-bsts, backtracking, advanced-dp · Code: C++
Per-pattern template: What it is → Intuition → Approach → Challenges & pitfalls → LC problems (max 3) → Worked examples.

The Master Insight (read before the patterns)

A graph is just states + transitions, and most graph interviews are two decisions: which traversal and what counts as a node. The second is the hidden skill — many problems never say "graph": Word Ladder's nodes are words, Open-the-Lock's nodes are 4-digit codes, a grid's nodes are cells. "Find the graph" — say what the nodes and edges are in one sentence — before touching any algorithm.

The routing table (memorize this — it answers "which algorithm?" for ~95% of problems): | The ask | The tool | |---|---| | Reachability / connected components / "count islands" | DFS or BFS (or DSU) — any of them | | Shortest path, unweighted (fewest steps/moves) | BFS — never DFS, never Dijkstra | | Shortest path, weighted, non-negative | Dijkstra | | Shortest path with negative edges, or "at most k hops" | Bellman-Ford (k-round variant) | | Order tasks with prerequisites / detect cycle in a DAG | Topological sort (Kahn's) | | Merge groups incrementally / "are these connected?" queries | Union-Find (DSU) | | Two-group assignment with conflict edges | Bipartite check (2-coloring) | | Connect everything at min total cost | MST (Prim/Kruskal) | | Distance from the nearest of many sources | Multi-source BFS |

Directed vs undirected changes cycle detection (colors vs parent-skip); weighted vs unweighted changes the shortest-path tool; that's most of the variation.

§0Conventions & Universal Pitfalls

// Adjacency list — the default representation
vector<vector<int>> adj(n);                       // or vector<vector<pair<int,int>>> weighted
for (auto& e : edges) { adj[e[0]].push_back(e[1]); adj[e[1]].push_back(e[0]); } // undirected: both

// Grid neighbors
const int DR[] = {-1, 1, 0, 0}, DC[] = {0, 0, -1, 1};
  1. Mark visited when you ENQUEUE, not when you dequeue. Marking at dequeue lets a node be enqueued many times before its first visit — on dense grids this silently turns O(V+E) BFS into a TLE. The single most common graph bug.
  2. BFS gives shortest paths only on unweighted graphs. The moment edges have weights, BFS's "first arrival = shortest" guarantee dies → Dijkstra. (Weights of only 0 and 1 → 0-1 BFS with a deque; name it, rarely needed.)
  3. Recursion depth: a 300×300 grid can produce a 90K-deep DFS → stack overflow in C++. For big grids, prefer BFS or an explicit stack; say why.
  4. Directed cycle detection needs three colors (white/gray/black — hitting a gray node = cycle). Undirected needs only visited + skip-parent (a visited non-parent neighbor = cycle). Using the undirected trick on a digraph reports false cycles on diamonds.
  5. Dijkstra in C++ = lazy deletion: priority_queue has no decrease-key — push duplicates, and on pop if (d > dist[u]) continue;. Forgetting the stale-skip line is the Dijkstra bug.
  6. DSU without both optimizations (path compression + union by size/rank) degrades to O(n) per op — write both by reflex; complexity answer: "amortized α(n), effectively constant."
  7. Level-aware BFS uses the queue-size snapshot (same as trees §0.5) or stores the distance with the node — pick one, don't mix.

1Grid Flood Fill / Connected Components

What it is: Treat a grid as a graph (cells = nodes, 4-neighbors = edges) and find/measure/paint connected regions: count islands, max island area, flood fill. One DFS/BFS from every unvisited land cell; each launch = one component.

Intuition: The outer double-loop plus "launch a traversal on unvisited land, mark everything reachable" is the component-counting algorithm — the traversal claims an entire island, so the number of launches equals the number of islands. Marking visited by mutating the grid ('1''0', "sinking" the island) saves the visited array and is idiomatic here — but say you're mutating the input and confirm it's allowed.

Approach:

int numIslands(vector<vector<char>>& g) {
    int m = g.size(), n = g[0].size(), count = 0;
    auto dfs = [&](auto&& self, int r, int c) -> void {
        if (r < 0 || r >= m || c < 0 || c >= n || g[r][c] != '1') return;
        g[r][c] = '0';                            // sink it = mark visited
        for (int k = 0; k < 4; ++k) self(self, r + DR[k], c + DC[k]);
    };
    for (int r = 0; r < m; ++r)
        for (int c = 0; c < n; ++c)
            if (g[r][c] == '1') { ++count; dfs(dfs, r, c); }
    return count;
}

Challenges & pitfalls

stack depth on large grids (§0.3 — offer the BFS version unprompted); bounds-check before reading the cell; 4- vs 8-connectivity (read the problem); area/perimeter variants just accumulate during the traversal — same skeleton, different bookkeeping.

LC 200 Number of IslandsLC 695 Max Area of IslandLC 733 Flood Fill
Example 1: Number of Islands (LC 200) — grid
1 1 0
0 1 0
0 0 1
  • Scan cells. First 1 at (0,0) → launch DFS: it sinks (0,0), (0,1), (1,1) — the whole connected blob — to 0. That's island 1.
  • Keep scanning; the next surviving 1 is at (2,2) → launch DFS, sink it. That's island 2.
  • No more land → 2 islands. ✓ Each DFS launch claims one entire component, so the launch count is the island count.

2Multi-Source BFS (distance from the nearest source)

What it is: "How long until all oranges rot?", "each cell's distance to the nearest 0" — distances measured from many sources at once. Seed the BFS queue with all sources at distance 0, then run one ordinary BFS; each cell's first visit is its distance to the nearest source.

Intuition: Imagine a virtual super-source wired to every real source with 0-cost edges — multi-source BFS is just single-source BFS from that phantom node. The wavefront expands from all sources at once, so frontiers meet in the middle; nothing else changes. The inverted framing is the skill: 542 asks "distance from each 1 to the nearest 0" — BFS from all the 0s outward, not from each 1 (which would be O((mn)²)).

Approach:

// 994: minutes until all oranges rot
queue<pair<int,int>> q;                    // seed with ALL rotten oranges
int fresh = 0, minutes = 0;
/* scan grid: push rotten, count fresh */
while (!q.empty() && fresh > 0) {
    int sz = q.size(); ++minutes;                     // one level = one minute
    for (int i = 0; i < sz; ++i) {
        auto [r, c] = q.front(); q.pop();
        for (int k = 0; k < 4; ++k) {
            int nr = r + DR[k], nc = c + DC[k];
            if (/*in bounds*/ && g[nr][nc] == 1) {
                g[nr][nc] = 2; --fresh; q.push({nr, nc});   // mark on enqueue (§0.1)
            }
        }
    }
}
return fresh ? -1 : minutes;

Challenges & pitfalls

the inversion (BFS from targets, not queries — the whole point of 542); unreachable cells (994's -1 case: track the fresh count, don't rescan); level counting off-by-one (trace a 2-cell example); seeding distance values when sources have distance 0 vs 1 (problem-dependent).

LC 994 Rotting OrangesLC 542 01 MatrixLC 1091 Shortest Path in Binary Matrix
Example 1: Rotting Oranges (LC 994) — grid (2 = rotten, 1 = fresh, 0 = empty):
2 1 1
1 1 0
0 1 1
  • Seed the queue with all rotten oranges: just (0,0). Fresh count = 6.
  • Minute 1: (0,0) rots its neighbors (0,1) and (1,0). Minute 2: those rot (0,2) and (1,1). Minute 3: (1,1) rots (2,1). Minute 4: (2,1) rots (2,2).
  • All fresh oranges rotted → 4 minutes. ✓ The wave spread from the single source level by level; if any fresh orange were unreachable, we'd return −1.

3BFS on Implicit State Graphs (the "find the graph" pattern)

What it is: Shortest transformation sequences where nodes are states and edges are legal moves: word → word by one letter (127), lock code → code by one wheel turn (752), puzzle board → board by one slide (773). No graph is given — you generate neighbors on the fly.

Intuition: "Minimum number of steps" over discrete states = unweighted shortest path = BFS, full stop. The design work is the neighbor function and the state encoding (a string, a tuple, a canonical serialization of a board). Complexity = O(states × neighbor-cost) — estimate the state count out loud (10⁴ lock codes, 9!/2 puzzle boards) to justify feasibility; that estimate is the interview. Bidirectional BFS (expand from both ends, always grow the smaller frontier) halves the exponent — name it for 127, implement only if pushed.

Approach (skeleton):

unordered_set<string> visited{start};
queue<string> q; q.push(start);
int steps = 0;
while (!q.empty()) {
    int sz = q.size();
    for (int i = 0; i < sz; ++i) {
        string cur = q.front(); q.pop();
        if (cur == target) return steps;
        for (string& nxt : neighbors(cur))          // generate moves; skip banned states
            if (visited.insert(nxt).second)          // insert-if-absent = mark on enqueue
                q.push(nxt);
    }
    ++steps;
}
return -1;

Challenges & pitfalls

neighbor generation cost (127: 26·L mutations against a dict set — not pairwise word comparison, which is O(n²·L)); banned/deadend states checked at generation (752's deadends — and the start itself may be a deadend); state canonicalization for boards (flatten 773's grid to a string); visited.insert().second fuses check+mark — the idiomatic C++ move.

LC 127 Word LadderLC 752 Open the LockLC 773 Sliding Puzzle
Example 1: Word Ladder (LC 127)begin = "hit", end = "cog", word list [hot, dot, dog, lot, log, cog]. Each step changes one letter to another valid word.
  • The graph: nodes are words, edges connect words differing by one letter. Run BFS from "hit":
    • level 1: hit. level 2: hot (change i→o). level 3: dot, lot. level 4: dog, log. level 5: cog.
  • Shortest transformation hit → hot → dot → dog → cog has 5 words. ✓ Neighbors are generated by trying all 26 letters at each position and checking the dictionary — never comparing every pair of words.

4BFS with Augmented State (position isn't enough)

What it is: Shortest path where the answer depends on more than location: "you may eliminate up to k obstacles" (1293), "visit all nodes" (847). The node becomes a tuple — (cell, eliminations left), (vertex, visited-mask) — and BFS proceeds normally over the bigger state space.

Intuition: When two arrivals at the same cell aren't interchangeable (one has budget left, one doesn't), the cell alone is an underspecified state — BFS's "first visit is best" guarantee only holds for the full tuple. This is the DP dimension-selection question applied to search: add exactly the dimensions that make states self-contained, no more. Prune dominated states where cheap (in 1293, arriving with more budget strictly dominates — track best-budget-per-cell instead of a full visited-set).

Approach (1293 core):

// state: (r, c, k_remaining); visited: best k seen per cell
vector<vector<int>> bestK(m, vector<int>(n, -1));
queue<array<int,3>> q; q.push({0, 0, k}); bestK[0][0] = k;
// standard level BFS; neighbor transition:
int nk = kCur - grid[nr][nc];                    // pay 1 to cross an obstacle
if (nk > bestK[nr][nc]) { bestK[nr][nc] = nk; q.push({nr, nc, nk}); }

Challenges & pitfalls

visited on the tuple, not the cell (the whole pattern — cell-only visited gives wrong answers; the dominance-pruned bestK is the efficient middle ground); state-space size math before coding (m·n·k for 1293; 2ⁿ·n for 847 — feasibility check aloud); early-exit shortcut in 1293 (k ≥ manhattan distance ⇒ answer is the manhattan distance).

LC 1293 Shortest Path with Obstacles EliminationLC 847 Shortest Path Visiting All Nodes
Example 1: Shortest Path with Obstacles Elimination (LC 1293) — grid
0 0 0
1 1 0
0 0 0
0 1 1
0 0 0

with k = 1 elimination, from top-left to bottom-right.

  • The state is (row, col, eliminations left), not just (row, col) — reaching a cell with 1 elimination left is genuinely different from reaching it with 0. BFS over these tuples.
  • The shortest route goes straight down the left/right corridors, spending its single elimination to punch through one wall, reaching the corner in 6 steps. ✓ If you tracked only (row, col) as visited, you'd wrongly block a longer-but-higher-budget arrival that the optimal path needs.

5Explicit-Graph Traversal (components, reachability)

What it is: The given-adjacency version of Pattern 1: count provinces from an adjacency matrix, decide if you can visit every room following keys, check if there's a path from A to B. Build/interpret the adjacency structure, run DFS/BFS, count launches or check reachability.

Intuition: Nothing new algorithmically — the pattern exists because representation handling is the tested skill: adjacency matrix (547) vs adjacency list (841) vs raw edge list (1971 — build the list first, or skip traversal entirely with DSU). Choosing DFS vs BFS vs DSU here is pure taste; saying "any of the three works — I'll use X because ⟨fewest lines / no recursion risk / queries suggest DSU⟩" is exactly the judgment sentence interviewers want.

Approach:

int findCircleNum(vector<vector<int>>& isConnected) {   // 547: adjacency MATRIX
    int n = isConnected.size(), comps = 0;
    vector<bool> vis(n, false);
    auto dfs = [&](auto&& self, int u) -> void {
        vis[u] = true;
        for (int v = 0; v < n; ++v)
            if (isConnected[u][v] && !vis[v]) self(self, v);
    };
    for (int u = 0; u < n; ++u)
        if (!vis[u]) { ++comps; dfs(dfs, u); }
    return comps;
}

Challenges & pitfalls

matrix traversal is O(n²) regardless of edge count (fine at n ≤ 200 — say it); edge-list inputs need the build step (forgetting both directions for undirected edges = the classic); self-loops/duplicate edges (harmless for reachability, but notice); directed reachability (841 is directed! — keys open rooms one-way; treating it as undirected happens to pass some cases and is still wrong).

LC 547 Number of ProvincesLC 841 Keys and RoomsLC 1971 Find if Path Exists in Graph
Example 1: Number of Provinces (LC 547)isConnected = [[1,1,0], [1,1,0], [0,0,1]] (a symmetric friendship matrix).
  • Cities 0 and 1 are directly connected; city 2 is connected only to itself.
  • DFS from 0 marks {0, 1} — that's province 1. City 2 is unvisited → new launch marks {2} — province 2.
  • Answer 2. ✓ Each DFS launch from an unvisited node claims one whole province.

6Topological Sort & Cycle Detection (DAG ordering)

What it is: Order items under prerequisite constraints, or decide it's impossible (a cycle): course schedule, build order, safe states. Kahn's algorithm: repeatedly take a node with indegree 0, emit it, decrement its neighbors' indegrees. Fewer emitted than n ⇒ cycle.

Intuition: A node with indegree 0 has no unmet prerequisites — safe to do now; doing it can only unlock others. If the queue empties early, every remaining node waits on another remaining node — that circular waiting is the cycle, and Kahn's detects it for free (no separate cycle pass). The DFS alternative (three colors, §0.4; reverse-post-order = topo order) is equivalent — know both, lead with Kahn's. 802 is the same idea on reversed edges: safe nodes = nodes that can't reach a cycle = run Kahn's on the reverse graph from the terminals.

Approach:

vector<int> findOrder(int n, vector<vector<int>>& pre) {  // 210
    vector<vector<int>> adj(n); vector<int> indeg(n, 0);
    for (auto& p : pre) { adj[p[1]].push_back(p[0]); ++indeg[p[0]]; }  // prereq → course
    queue<int> q;
    for (int u = 0; u < n; ++u) if (!indeg[u]) q.push(u);
    vector<int> order;
    while (!q.empty()) {
        int u = q.front(); q.pop(); order.push_back(u);
        for (int v : adj[u]) if (--indeg[v] == 0) q.push(v);
    }
    return (int)order.size() == n ? order : vector<int>{};  // short ⇒ cycle ⇒ impossible
}

Challenges & pitfalls

edge direction from the input format ([a, b] = "b before a" in 207/210 — get it backwards and everything inverts; restate it before building); the size check as the cycle test (no extra machinery); multiple valid orders (any is accepted; lexicographic-smallest needs a min-heap queue); recognizing costumes ("alien dictionary": char-order constraints from adjacent word pairs → topo sort).

LC 207 Course ScheduleLC 210 Course Schedule IILC 802 Find Eventual Safe States
Example 1: Course Schedule (LC 207) — 2 courses, prerequisite [1, 0] (must take 0 before 1). Can you finish?
  • Indegrees: course 0 has 0, course 1 has 1. Queue starts with [0].
  • Take 0, emit it; decrement course 1's indegree to 0 → enqueue 1. Take 1.
  • Emitted 2 courses = n → no cycle → true. ✓ With prerequisites [[1,0], [0,1]] instead, both start with indegree 1, the queue is empty from the start, nothing is emitted → cycle → false.

7Union-Find / DSU (incremental connectivity)

What it is: A forest of parent pointers answering "are x and y in the same group?" and "merge these groups" in near-O(1): the redundant connection (the edge that closes a cycle), account merges (emails as nodes), removing max stones (row/col as meta-nodes). The tool of choice when edges arrive incrementally or the problem is phrased as merging.

Intuition: Each group is a tree; find walks to the root (with path compression: point everyone at the root as you return); union hangs the smaller tree under the larger (by size/rank). Together: amortized α(n) ≈ constant. DSU vs DFS: DFS answers connectivity for a fixed graph; DSU stays correct as edges stream in — 684 is literally "the first edge whose endpoints already connect." The modeling trick that unlocks hard DSU problems: nodes needn't be the obvious entities — union stones with row-id and col-id meta-nodes (947), union emails (721), and components fall out.

Approach:

struct DSU {
    vector<int> p, sz;
    DSU(int n) : p(n), sz(n, 1) { iota(p.begin(), p.end(), 0); }
    int find(int x) { return p[x] == x ? x : p[x] = find(p[x]); }   // path compression
    bool unite(int a, int b) {
        a = find(a); b = find(b);
        if (a == b) return false;                  // already connected — 684's answer moment
        if (sz[a] < sz[b]) swap(a, b);
        p[b] = a; sz[a] += sz[b];
        return true;
    }
};

Challenges & pitfalls

both optimizations by reflex (§0.6) and the α(n) complexity claim; string-keyed problems need an id map first (721 — email → int before DSU); counting components after unions = "number of nodes that are their own parent" after a final find-pass (stale parents lie mid-stream); DSU can't split — recognizing "this needs splitting, DSU won't work" is in scope.

LC 684 Redundant ConnectionLC 721 Accounts MergeLC 947 Most Stones Removed
Example 1: Redundant Connection (LC 684) — edges [[1,2], [1,3], [2,3]]; return the edge that creates a cycle.
  • Process edges in order, uniting endpoints:
    • [1,2]: 1 and 2 were separate → unite them.
    • [1,3]: 1 and 3 separate → unite (now {1,2,3} are one group).
    • [2,3]: find(2) and find(3) are already the same root → this edge closes a cycle → return [2,3]. ✓
  • DSU answers "already connected?" in near-constant time as each edge streams in — exactly what a static DFS couldn't do incrementally.

8Bipartite Check / 2-Coloring

What it is: Can the nodes be split into two groups so every edge crosses between groups? (Equivalently: is there no odd cycle?) BFS/DFS-color each node opposite to its parent; an edge joining two same-colored nodes = not bipartite.

Intuition: If a two-group split exists, the coloring is forced once a component's first node is colored — so greedy propagation either completes (bipartite) or hits a contradiction (an odd cycle, the only obstruction). The recognition skill: problems that never say "bipartite" — "split people into two teams given rivalries" (886) is this; conflict-edges + two groups = 2-coloring.

Approach:

bool isBipartite(vector<vector<int>>& adj) {
    int n = adj.size();
    vector<int> color(n, -1);
    for (int s = 0; s < n; ++s) {                  // graph may be disconnected!
        if (color[s] != -1) continue;
        queue<int> q; q.push(s); color[s] = 0;
        while (!q.empty()) {
            int u = q.front(); q.pop();
            for (int v : adj[u]) {
                if (color[v] == -1) { color[v] = color[u] ^ 1; q.push(v); }
                else if (color[v] == color[u]) return false;
            }
        }
    }
    return true;
}

Challenges & pitfalls

the disconnected-graph outer loop (785's tests specifically catch its absence); the color[u] ^ 1 flip idiom; the odd-cycle explanation on demand ("walking a cycle flips color each step — an odd length returns to the start demanding both colors"); when counts per group matter (variants), collect component group-sizes and combine.

LC 785 Is Graph Bipartite?LC 886 Possible Bipartition
Example 1: Is Graph Bipartite? (LC 785)graph = [[1,3], [0,2], [1,3], [0,2]] (a 4-cycle 0–1–2–3–0).
  • Color 0 → A. Its neighbors 1 and 3 → B. Their neighbors: node 2 (neighbor of both 1 and 3) → A.
  • Check every edge: 0(A)–1(B), 1(B)–2(A), 2(A)–3(B), 3(B)–0(A) — all cross between groups, no conflict → true. ✓ (A triangle 0–1–2–0 would force node 2 to be both colors → false; that odd cycle is the only thing that can break bipartiteness.)

9Weighted Shortest Paths (Dijkstra & Bellman-Ford)

What it is: Cheapest path when edges have costs: network delay (743), cheapest flight with ≤ k stops (787), minimize the maximum step (1631). Dijkstra = BFS upgraded with a priority queue (settle nodes cheapest-first); Bellman-Ford = relax all edges in rounds (round i = best using ≤ i edges).

Intuition: Dijkstra's invariant: with non-negative weights, the unsettled node with the smallest tentative distance can't be improved (any detour costs ≥ 0 more) — so settle it. That argument dies with negative edges or hop limits: 787's "≤ k stops" makes Dijkstra's greedy commitment wrong (a pricier-but-shorter prefix may win later) — Bellman-Ford's round structure is the hop count, run k+1 rounds against a frozen-snapshot copy. 1631 shows the objective swap: minimize the max edge instead of the sum — Dijkstra works with alt = max(dist[u], w).

Approach (Dijkstra with lazy deletion — §0.5):

vector<int> dist(n + 1, INT_MAX); dist[src] = 0;
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<>> pq;  // (dist, node)
pq.push({0, src});
while (!pq.empty()) {
    auto [d, u] = pq.top(); pq.pop();
    if (d > dist[u]) continue;                     // stale entry — the mandatory line
    for (auto [v, w] : adj[u])
        if (dist[u] + w < dist[v]) { dist[v] = dist[u] + w; pq.push({dist[v], v}); }
}

Challenges & pitfalls

the stale-skip line (§0.5 — its absence is the Dijkstra bug); min-heap direction (greater<> — C++ pq defaults to max); why-not-Dijkstra for 787 (rehearse the hop-limit argument; also the Bellman-Ford round must read the previous round's array — in-place relaxation lets one round use multiple edges); overflow on dist[u] + w when the sentinel is INT_MAX (guard or use long long); complexity O(E log V).

LC 743 Network Delay TimeLC 787 Cheapest Flights Within K StopsLC 1631 Path With Minimum Effort
Example 1: Cheapest Flights Within K Stops (LC 787)n = 3, flights [[0,1,100], [1,2,100], [0,2,500]], from 0 to 2 with at most k = 1 stop.
  • Bellman-Ford, because the hop limit breaks Dijkstra's greediness. Start dist = [0, ∞, ∞].
    • Round 1 (paths of ≤ 1 edge, from a frozen copy): relax 0→1dist[1]=100; relax 0→2dist[2]=500.
    • Round 2 (≤ 2 edges = ≤ 1 stop): relax 1→2 using round-1's dist[1]=100dist[2] = min(500, 200) = 200.
  • Cheapest with ≤ 1 stop = 200 (0 → 1 → 2). ✓ Running exactly k+1 = 2 rounds off a frozen snapshot is what enforces the stop limit — plain Dijkstra could have locked in the direct 500 flight too early.

10Minimum Spanning Tree

What it is: Connect all nodes at minimum total edge cost (1584: cities as points, cost = manhattan distance). Kruskal's: sort edges, greedily take any edge joining two different components (DSU — Pattern 7 as a subroutine). Prim's: grow one tree, always adding the cheapest edge leaving it (Dijkstra's skeleton with key = w instead of dist + w).

Intuition: Both are the cut property: the cheapest edge crossing any cut is safe to take (exchange argument: any spanning tree avoiding it can swap it in with no cost increase — deliver this in two sentences when asked "why is greedy correct?"). Kruskal = the cut property applied globally (sort once); Prim = applied to the growing-tree cut. Choose by shape: sparse/edge-list given → Kruskal; dense/complete graphs (1584's n² point pairs) → Prim with the O(n²) array version, no heap needed.

Approach (Kruskal core):

sort(edges.begin(), edges.end());                  // (w, u, v)
DSU dsu(n);
int total = 0, used = 0;
for (auto& [w, u, v] : edges)
    if (dsu.unite(u, v)) { total += w; if (++used == n - 1) break; }
return total;

Challenges & pitfalls

MST ≠ shortest paths (an MST path between two nodes can be far longer than their shortest path — one sentence distinguishing them defuses the trap); early exit at n−1 edges; dense-graph edge generation cost for 1584 (n² edges to sort — mention Prim-array as the cleaner fit even if you code Kruskal); disconnected input ⇒ no spanning tree (check used == n - 1).

LC 1584 Min Cost to Connect All Points
Example 1: Min Cost to Connect All Points (LC 1584) — points [[0,0], [2,2], [3,10], [5,2], [7,0]], cost between two points = manhattan distance.
  • Kruskal: generate all pairwise manhattan distances, sort ascending, and take each edge that joins two still-separate components (DSU), stopping after n − 1 = 4 edges.
  • The cheapest connecting set of 4 edges (e.g. (0,0)-(2,2)=4, (2,2)-(5,2)=3, (5,2)-(7,0)=4, (2,2)-(3,10)=9) totals 20. ✓ The cut property guarantees that greedily taking the cheapest cross-component edge never overshoots the optimum.

11Graph Construction & Relational Costumes

What it is: Problems where the work is modeling: clone a graph (133 — traverse while duplicating, a hashmap old→new as the visited set), evaluate division (399 — a/b = 2.0 is a weighted edge; a query a/c is a path whose answer is the product of edge weights along it).

Intuition: 133's insight: the old→new map is the visited marker — create-on-first-encounter, reuse ever after; wiring neighbors happens naturally in traversal order. 399's insight is the transferable one: relations compose along paths — division ratios multiply, currency conversions multiply, "same as" relations just connect. Whenever pairwise relations must be chained, build the graph and let traversal (or weighted DSU) do the algebra. Costume-spotting is the entire skill this pattern trains.

Approach (133 core):

unordered_map<Node*, Node*> mp;                    // old → new; doubles as visited
Node* clone(Node* u) {
    if (!u) return nullptr;
    if (auto it = mp.find(u); it != mp.end()) return it->second;
    Node* nu = new Node(u->val);
    mp[u] = nu;                                    // register BEFORE recursing — cycles!
    for (Node* v : u->neighbors) nu->neighbors.push_back(clone(v));
    return nu;
}

Challenges & pitfalls

registering the clone before recursing (after ⇒ infinite loop on any cycle — the bug of 133); 399's absent-variable queries (return −1.0; check both endpoints exist before traversing); floating-point path products (fine here; note precision awareness); bidirectional edges with reciprocal weights (a/b = 2 ⇒ b/a = 0.5 — add both).

LC 133 Clone GraphLC 399 Evaluate Division
Example 1: Evaluate Division (LC 399) — given a / b = 2.0 and b / c = 3.0, answer queries.
  • Build a weighted graph: edge a → b weight 2 (and b → a weight 0.5), edge b → c weight 3 (and c → b weight 1/3).
  • Query a / c: walk a path a → b → c, multiplying weights → 2.0 × 3.0 = 6.0.
  • Query b / a: path b → a0.5. Query a / e: e isn't in the graph → −1.0.
  • Relations chaining along a path (ratios multiplying) is the reusable idea — same shape as currency conversion. ✓

A Note on the Specialists (know the names, not the code)

Summary Table — Recognition Cheat Sheet

#PatternTrigger phraseToolComplexity
1Flood fill"islands", "regions", "paint"DFS/BFS per launchO(mn)
2Multi-source BFS"nearest ⟨source⟩", "spread/rot/infect"seed all sources at onceO(mn)
3Implicit-state BFS"min steps to transform/reach"find-the-graph + BFSO(states·moves)
4Augmented-state BFS"with at most k ⟨budget⟩"tuple state, tuple visitedO(states·dims)
5Explicit traversal"provinces", "can you reach"DFS/BFS/DSU — anyO(V+E)
6Topo sort"prerequisites", "order of tasks"Kahn's; short output ⇒ cycleO(V+E)
7Union-Find"merge", "redundant edge", incrementalDSU (compress + by size)O(α(n)/op)
8Bipartite"two groups", "rivals apart"2-coloring BFSO(V+E)
9Weighted shortest"cheapest/fastest", weightsDijkstra; k-hops ⇒ Bellman-FordO(E log V)
10MST"connect all, min cost"Kruskal/Prim (cut property)O(E log E)
11Modeling"clone", relations that chainmap old→new; relations as edgesO(V+E)
Study order: 1 → 2 → 5 (traversal fluency) → 6 (the DAG staple) → 7 (DSU by reflex) → 3 (find-the-graph — the differentiating skill) → 9 (Dijkstra + the 787 why-not) → 8 → 4 → 10 → 11 → specialists.
The interview sentence (per your communication playbook §1.3): "Let me name the graph first: nodes are ⟨X⟩, edges are ⟨Y⟩, ⟨directed/undirected, weighted/unweighted⟩. The ask is ⟨shortest path / ordering / connectivity⟩, which routes to ⟨BFS / topo sort / DSU⟩ — that's O(⟨V+E⟩), and the state space is about ⟨estimate⟩ so it's feasible."