LC Patterns — Graphs
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.
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};- 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.
- 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.)
- 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.
- 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.
- Dijkstra in C++ = lazy deletion:
priority_queuehas no decrease-key — push duplicates, and on popif (d > dist[u]) continue;. Forgetting the stale-skip line is the Dijkstra bug. - 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."
- 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.
1 1 0
0 1 0
0 0 1- Scan cells. First
1at (0,0) → launch DFS: it sinks (0,0), (0,1), (1,1) — the whole connected blob — to0. That's island 1. - Keep scanning; the next surviving
1is 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).
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.
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 → coghas 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).
0 0 0
1 1 0
0 0 0
0 1 1
0 0 0with 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).
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).
[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.
[[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)andfind(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.
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).
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→1→dist[1]=100; relax0→2→dist[2]=500. - Round 2 (≤ 2 edges = ≤ 1 stop): relax
1→2using round-1'sdist[1]=100→dist[2] = min(500, 200) = 200. - Cheapest with ≤ 1 stop = 200 (
0 → 1 → 2). ✓ Running exactlyk+1 = 2rounds 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).
[[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 = 4edges. - 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).
a / b = 2.0 and b / c = 3.0, answer queries.- Build a weighted graph: edge
a → bweight 2 (andb → aweight 0.5), edgeb → cweight 3 (andc → bweight 1/3). - Query
a / c: walk a patha → b → c, multiplying weights →2.0 × 3.0 =6.0. - Query
b / a: pathb → a→ 0.5. Querya / e:eisn'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)
- Bridges / articulation points (Tarjan's low-link): 1192 (Critical Connections) — edges whose removal disconnects the graph; DFS with discovery times and low-links. Low frequency; recognize + sketch, implement only if the loop demands it.
- Strongly connected components (Tarjan/Kosaraju): condensing a digraph into a DAG of SCCs — name it when a directed problem wants "mutually reachable groups."
- A\*: Dijkstra + an admissible heuristic — name-drop for grid pathfinding follow-ups ("manhattan-distance heuristic"), never required on LC.
- Eulerian path (Hierholzer): 332 (Reconstruct Itinerary) — "use every edge exactly once"; greedy DFS + post-order insert. One-problem pattern, worth one read.
▤Summary Table — Recognition Cheat Sheet
| # | Pattern | Trigger phrase | Tool | Complexity |
|---|---|---|---|---|
| 1 | Flood fill | "islands", "regions", "paint" | DFS/BFS per launch | O(mn) |
| 2 | Multi-source BFS | "nearest ⟨source⟩", "spread/rot/infect" | seed all sources at once | O(mn) |
| 3 | Implicit-state BFS | "min steps to transform/reach" | find-the-graph + BFS | O(states·moves) |
| 4 | Augmented-state BFS | "with at most k ⟨budget⟩" | tuple state, tuple visited | O(states·dims) |
| 5 | Explicit traversal | "provinces", "can you reach" | DFS/BFS/DSU — any | O(V+E) |
| 6 | Topo sort | "prerequisites", "order of tasks" | Kahn's; short output ⇒ cycle | O(V+E) |
| 7 | Union-Find | "merge", "redundant edge", incremental | DSU (compress + by size) | O(α(n)/op) |
| 8 | Bipartite | "two groups", "rivals apart" | 2-coloring BFS | O(V+E) |
| 9 | Weighted shortest | "cheapest/fastest", weights | Dijkstra; k-hops ⇒ Bellman-Ford | O(E log V) |
| 10 | MST | "connect all, min cost" | Kruskal/Prim (cut property) | O(E log E) |
| 11 | Modeling | "clone", relations that chain | map old→new; relations as edges | O(V+E) |