DSA Notes · chapter 01 · start here · Java Live Code

Graphs, with both examples and both Javas

Highest-value cluster for a leftover coding hour. Every algorithm below has a simple Java version, an interview Java version, a tiny example, and a second example that does not skip a step. DFS comments say when a node is marked and what a returning stack frame still has to do. After the dry-run, the notebook rows jump into named-IE Java — other candidates’ families, not a Job 10454435 list.

Prev: How to use · Next: 02 Trees

01 — start here

Nine graph algorithms

Representation, BFS, DFS (recursive and iterative), directed vs undirected cycle, Kahn / DFS topo, Union-Find, Dijkstra, Bellman-Ford, Kruskal / Prim. A grid is a graph. Mark BFS on enqueue. Mark DFS on entry.

Representation — adjacency list and grids

Recognize it from

A list of edges, a matrix of 0/1 cells, words like reachable / connected / spread / shortest on a grid. Four questions: directed?, weighted?, cyclic?, connected?

What it actually does

A graph is nodes plus edges. An adjacency list stores, for each node, the nodes you can walk to in one step. Interview graphs are almost always sparse, so a list is the default: space O(V+E) instead of O(V²) for a matrix of “no edge” cells.

A grid is the same object with a different costume. Each cell is a node. The four (sometimes eight) in-bound neighbors are edges. You rarely build adj explicitly — you compute neighbors from dirs when you need them.

Simple — read this first

// n = 3 nodes, undirected edges 0-1 and 1-2
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>()); // one bucket per node id
adj.get(0).add(1);
adj.get(1).add(0); // undirected: write both directions
adj.get(1).add(2);
adj.get(2).add(1);

Interview — type this

List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
for (int[] e : edges) {
    adj.get(e[0]).add(e[1]);
    adj.get(e[1]).add(e[0]); // delete this line for a directed graph
}
int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}}; // grid: right, left, down, up
// cell (r,c) is a node; neighbor (r+dr, c+dc) is an edge if in bounds
Example 1 — simple

Three nodes, two undirected edges. Node ids are 0, 1, 2.

0 — 1 — 2 adj[0] = [1] adj[1] = [0, 2] adj[2] = [1]
Build the list from edges (0,1) and (1,2).
Edge writtenadj[0]adj[1]adj[2]
start[][][]
0-1 both ways[1][0][]
1-2 both ways[1][0, 2][1]

From 0 you can only reach 1 in one step. From 1 you can reach 0 or 2. There is no 0–2 edge.

Example 2 — every step

A 2×3 grid. Cell (r,c) is a node. Walk every neighbor of the center-left cell (1,1) with dirs.

row0: A B C coordinates: (0,0) (0,1) (0,2) row1: D E F (1,0) (1,1) (1,2) Stand on E = (1,1). Four tries:
Every dir from (1,1). Out-of-bounds is not an edge.
dir (dr,dc)candidatein bounds?edge?
(0,1) right(1,2) Fyesyes — E–F
(0,−1) left(1,0) Dyesyes — E–D
(1,0) down(2,1)no — only 2 rowsskip
(−1,0) up(0,1) Byesyes — E–B

E’s neighbors are B, D, F. Same BFS/DFS as on adj, with bounds checks instead of a list lookup.

Time Build list O(V+E) · neighbor of a grid cell O(1)Space List O(V+E) · matrix O(V²) · grid dirs O(1)
Why this

Reach for a matrix only when you need O(1) “is u–v an edge?” on a dense graph. Amazon Live Code almost always hands you a sparse edge list or a grid.

Common mistakes
  • Forgetting the reverse edge on an undirected graph — DFS/BFS then cannot walk back, so you miss half the component.
  • Using 1-based ids from the prompt in a 0-based ArrayList without allocating n+1 buckets.
  • On a grid, skipping the in-bounds check and then indexing grid[-1][c].
ProblemPatternLinks
Number of Islands (grid-as-graph)cell = node, 4-dir = edgecheat card · clue · clue · clue
Clone Graphcopy nodes while walking adjcheat card · clue

Breadth-First Search (BFS)

Recognize it from

“Shortest path,” “minimum steps / moves / operations,” “fewest minutes until …,” in an unweighted graph or grid. Rotting oranges, word ladder, open-the-lock, knight hops, reduce-n-by-a-digit.

What it actually does

BFS explores every node at distance 1 before any node at distance 2. A queue (FIFO) is what makes that true: you finish the older, closer nodes before the newer, farther ones. The first time you reach a node is therefore via a shortest path. Weighted edges break this — then you need Dijkstra or Bellman-Ford.

Mark a node visited when you enqueue it, not when you poll it. If you wait until poll, two parents at the same distance can both push the same child, and the queue fills with duplicates. On a grid that mistake turns an O(mn) walk into a much larger bag of repeated cells.

Simple — read this first

void bfs(List<List<Integer>> adj, int start, boolean[] visited) {
    Queue<Integer> q = new ArrayDeque<>();
    visited[start] = true; // mark on ENQUEUE — before anyone else can push start again
    q.offer(start);
    while (!q.isEmpty()) {
        int u = q.poll(); // FIFO: oldest (closest) node comes out first
        for (int v : adj.get(u)) {
            if (!visited[v]) {
                visited[v] = true; // mark NOW, not when we poll v later
                q.offer(v);
            }
        }
    }
}

Interview — type this

int[] bfsDist(List<List<Integer>> adj, int start, int n) {
    int[] dist = new int[n];
    Arrays.fill(dist, -1); // -1 = not reached (also the visited flag)
    Queue<Integer> q = new ArrayDeque<>();
    dist[start] = 0;
    q.offer(start);
    while (!q.isEmpty()) {
        int u = q.poll();
        for (int v : adj.get(u)) {
            if (dist[v] == -1) { // first time we see v — this path is shortest
                dist[v] = dist[u] + 1;
                q.offer(v);
            }
        }
    }
    return dist; // -1 remains for unreachable nodes
}
// Multi-source: offer EVERY source first (all rotten oranges, all gates), dist=0,
// then the same loop. One layer of the queue = one minute / one step.
Example 1 — simple

Line graph, start at 0. You should be able to finish this table without looking at the code.

0 — 1 — 2 start = 0
BFS from 0. Mark on enqueue.
Steppollqueue beforeneighborsqueue aftervisited
init———[0]{0}
10[0]1 new[1]{0,1}
21[1]0 seen, 2 new[2]{0,1,2}
32[2]1 seen[]unchanged

Visit order 0, 1, 2. Distances from 0: [0, 1, 2]. There is no shorter way to 2 than two edges.

Example 2 — every step

Same drawing the rest of this chapter reuses. Start at 0. Notice 3 and 4 are both distance 2 — BFS must finish 1 and 2 before touching them.

0 / \ 1 2 | | 3---4 edges: 0-1, 0-2, 1-3, 2-4, 3-4
Every poll. Neighbor order assumed [smaller id first] as in a typical adj build.
Steppollqueue beforeneighbors checkedqueue afterdist[]
init———[0][0, −1, −1, −1, −1]
10[0]1 new (d=1), 2 new (d=1)[1, 2][0, 1, 1, −1, −1]
21[1, 2]0 seen, 3 new (d=2)[2, 3][0, 1, 1, 2, −1]
32[2, 3]0 seen, 4 new (d=2)[3, 4][0, 1, 1, 2, 2]
43[3, 4]1 seen, 4 seen (already d=2)[4]unchanged
54[4]2 seen, 3 seen[]unchanged

Final distances from 0: [0, 1, 1, 2, 2]. Visit order 0, 1, 2, 3, 4. The edge 3–4 is examined but never improves a distance — both ends were already reached on a shorter path. Swap the queue for a stack and you get DFS, which on this graph visits 0, 1, 3, 4, 2 and loses the shortest-path guarantee if edges had different lengths (even here the visit order is no longer by distance).

Time O(V+E) — each node enqueued once, each edge looked at onceSpace O(V) for the queue and dist/visited
Why this

A queue is not a style choice. FIFO is the invariant “closer nodes leave first.” A stack is LIFO: you dive down a branch. That is DFS. Multi-source BFS is the same loop after you offer every source at distance 0 — that is rotting oranges (all 2s at minute 0) and “walls and gates.”

Common mistakes
  • Marking visited on poll — the same cell is pushed many times; distances can still be right but the queue explodes, and some codes then overwrite dist.
  • Forgetting to snapshot q.size() when the prompt asks for minutes / levels, not just reachability.
  • Using DFS for unweighted shortest path because “it also visits every node.” It does, but not in distance order.
ProblemPatternLinks
Rotten Orangesmulti-source BFS, one layer = one minuteJava in ch.05 · cheat card · clue · clue
Open the Lock (similar)BFS on 4-digit states, deadends blockedJava in ch.05 · cheat card
Knight moves / generalized deltasBFS hops, different DIRSJava in ch.05 · cheat card · cheat card
Min ops reduce n by a digit of nBFS on integers 0..nJava in ch.05 · cheat card · clue
Word Ladder CAT→MENBFS on words, one-letter edgescheat card · cheat card · clue · R4 coding
Distance K in a treebuild undirected view, BFS from targetJava in ch.05 · cheat card · clue · R4 coding
Keys and Roomsreachability BFS/DFS from room 0cheat card · clue
Burning Treetree as undirected, BFS layers from a start nodecheat card
Race CarBFS on (position, speed) state — not a plain gridcheat card

Depth-First Search (DFS)

Recognize it from

Connected components, “does a path exist,” number of islands, clone a graph, exhaustive exploration, and the skeleton of backtracking. Interviewers sometimes ask for the iterative version to check you know what recursion is doing.

What it actually does

DFS fully explores one neighbor’s branch before trying the next neighbor. Recursion makes that automatic: dfs(v) does not return until everything reachable from v through unused nodes is finished. The Java call stack holds one frame per node on the current path — that frame remembers which node and where in the neighbor loop you are. That is why a chain of V nodes uses O(V) stack space, not “some small height H.”

Mark visited on entry (the first time you arrive). If you mark on exit, a cycle can recurse forever because the neighbor still looks unused. Iterative DFS uses an explicit Deque as a stack. Order can differ from recursive DFS unless you push neighbors in reverse so the first neighbor is the first one popped.

BFS vs DFS on the same graph is the picture to remember: BFS walks by distance; DFS walks by “go left until you cannot.” Neither is “better.” Shortest unweighted path is BFS. Components, cycle-in-directed (3-state), and backtracking are DFS.

Simple — read this first

void dfs(int u, List<List<Integer>> adj, boolean[] visited, List<Integer> order) {
    visited[u] = true; // arriving for the FIRST time — mark before neighbors
    order.add(u); // preorder: record on entry (postorder would add after the loop)
    for (int v : adj.get(u)) {
        if (!visited[v]) {
            dfs(v, adj, visited, order); // go DEEP. this call returns only when v's branch is done
        }
        // if visited[v] is true, v is on this path or an earlier finished path — skip
    }
    // no more unused neighbors. this stack frame pops. the caller continues ITS for-loop
}
// start: Arrays.fill(visited,false); dfs(start, adj, visited, order);

Interview — type this

// Recursive — the JVM call stack IS the stack. Loop all nodes for a disconnected forest.
void dfsAll(List<List<Integer>> adj, int n, List<Integer> order) {
    boolean[] visited = new boolean[n];
    for (int i = 0; i < n; i++) {
        if (!visited[i]) dfs(i, adj, visited, order); // new component
    }
}
void dfs(int u, List<List<Integer>> adj, boolean[] vis, List<Integer> order) {
    vis[u] = true;
    order.add(u);
    for (int v : adj.get(u)) {
        if (!vis[v]) dfs(v, adj, vis, order);
    }
}

// Iterative — same idea, Deque used as a stack. A node can be pushed twice;
// skip if already visited when it is popped (unlike BFS, we often mark on POP here
// because we push neighbors in reverse to mimic recursive left-to-right).
List<Integer> dfsIterative(List<List<Integer>> adj, int start, int n) {
    List<Integer> order = new ArrayList<>();
    boolean[] vis = new boolean[n];
    Deque<Integer> st = new ArrayDeque<>();
    st.push(start);
    while (!st.isEmpty()) {
        int u = st.pop();
        if (vis[u]) continue; // duplicate push from two parents
        vis[u] = true;
        order.add(u);
        List<Integer> nbrs = adj.get(u);
        for (int i = nbrs.size() - 1; i >= 0; i--) { // reverse so first neighbor is popped first
            int v = nbrs.get(i);
            if (!vis[v]) st.push(v);
        }
    }
    return order;
}
Example 1 — simple

Same 3-node line as BFS Example 1, so you can compare. Recursive DFS from 0. Neighbor lists: 0→[1], 1→[0,2], 2→[1].

Call stack grows DOWN the page. A ↩ line is a return. dfs(0) mark 0 order=[0] neighbor 1 unused → call dfs(1) dfs(1) mark 1 order=[0,1] neighbor 0 already visited neighbor 2 unused → call dfs(2) dfs(2) mark 2 order=[0,1,2] neighbor 1 visited → loop ends → return ↩ dfs(1) no more neighbors → return ↩ dfs(0) no more neighbors → return
Every call and every return. This is the whole algorithm.
Eventuvisited afterorderwhat happens next
enter dfs(0)0{0}[0]for-loop: only neighbor 1
enter dfs(1)1{0,1}[0, 1]neighbors [0, 2]; 0 is visited → skip
enter dfs(2)2{0,1,2}[0, 1, 2]neighbor 1 visited → loop empty
return dfs(2)2unchangedunchangedframe pops; dfs(1) continues the for-loop
return dfs(1)1unchangedunchangedno more neighbors
return dfs(0)0unchangedunchangeddone

DFS order 0, 1, 2 — on a line it matches BFS. The difference shows up as soon as a node has two unused neighbors (Example 2).

Example 2 — every step

The diamond. Recursive DFS from 0, neighbors in increasing id: 0→[1,2], 1→[0,3], 2→[0,4], 3→[1,4], 4→[2,3]. Watch the stack go down 0-1-3-4-2, then unwind, and see that when dfs(0) would try neighbor 2, 2 is already visited via 4.

0 / \ 1 2 | | 3---4
Recursive DFS from 0. ↩ is a return. No skipped calls.
Eventnodeactionvisitedorder
enter dfs(0)0mark; neighbors [1,2] — recurse 1 first{0}[0]
enter dfs(1)1mark; [0,3] — 0 seen, recurse 3{0,1}[0, 1]
enter dfs(3)3mark; [1,4] — 1 seen, recurse 4{0,1,3}[0, 1, 3]
enter dfs(4)4mark; [2,3] — recurse 2 (3 later, already seen){0,1,3,4}[0, 1, 3, 4]
enter dfs(2)2mark; [0,4] — both already visited, return{0,1,3,4,2}[0, 1, 3, 4, 2]
↩ in dfs(4)4neighbor 3 already visited; returnunchangedunchanged
↩ in dfs(3)3no more neighbors; returnunchangedunchanged
↩ in dfs(1)1no more neighbors; returnunchangedunchanged
↩ in dfs(0)0neighbor 2 already visited (found via 1-3-4); returnunchangedunchanged

DFS visit order: 0, 1, 3, 4, 2. BFS on the same drawing was 0, 1, 2, 3, 4. Both visit every node; only BFS groups them by distance from 0.

Iterative stack after pushing 0, if we push neighbors in reverse so 1 is popped first: pop 0, push 2 then 1 (reverse of [1,2]), pop 1, … — you can match the recursive order. If you push left-to-right you get a different but still valid DFS.

Time O(V+E)Space O(V) recursion stack in the worst case (a single path)
Why this

Recursion “just works” because each call stores the neighbor-loop index in a stack frame. The iterative Deque stores the nodes; it does not automatically store “I was in the middle of the neighbor list,” which is why people push all unused neighbors at once and why order can differ. For islands, the recursive version is the one you type: DFS from a land cell, sink or mark the whole component, count how many times you started.

Common mistakes
  • Forgetting visited[u]=true before the neighbor loop — a 2-cycle 0⇄1 recurses forever.
  • Not looping for i in 0..n-1 on a disconnected graph — you only flood the component of node 0.
  • Using recursive DFS on a 10^5-long chain in Java without raising stack size — you will StackOverflow; say “iterative / explicit stack” if n is huge.
  • Treating DFS as shortest path. It is not, unless every edge has the same weight and you still got lucky with order.
ProblemPatternLinks
Number of IslandsDFS/BFS flood from each unvisited landcheat card · clue · clue · clue
Clone GraphDFS/BFS + map original→copycheat card · clue
Longest increasing path in a matrixmemo DFS, 4-dir to a strictly larger cellJava in ch.05 · cheat card
Keys and Rooms / graph BFS-DFS familyreachability from 0cheat card · cheat card
Detonate the Maximum Bombsbuild graph by range, DFS/BFS component sizecheat card
Currency converterweighted DFS/BFS, product along a pathcheat card · clue
Bipartite / two groupsDFS/BFS coloring, conflict = odd cyclecheat card · clue

Cycle detection — directed vs undirected

Recognize it from

“Detect a cycle,” “can you finish all courses,” anything that must prove a graph is a DAG before topological sort. Directed and undirected are different algorithms. A boolean visited array is not enough on a directed graph.

What it actually does

Undirected: every edge is two-way, so the edge you just walked, back to your parent, is not a cycle. A cycle is “I reached an already-visited node that is not my parent.”

Directed: there is no “parent edge is harmless” rule. You must know whether a node is on the active recursion path (gray) versus fully finished (black). An edge into a gray node is a back-edge = cycle. An edge into a black node is a cross/forward edge into an already-safe region — not a cycle.

Simple — read this first

// UNDIRECTED — the edge back to parent is not a cycle
boolean cycleU(int u, int parent, List<List<Integer>> adj, boolean[] vis) {
    vis[u] = true;
    for (int v : adj.get(u)) {
        if (!vis[v]) {
            if (cycleU(v, u, adj, vis)) return true;
        } else if (v != parent) {
            return true; // visited and not where we came from → a back edge
        }
    }
    return false;
}

Interview — type this

// DIRECTED — 0 unseen, 1 on the CURRENT path (gray), 2 finished (black)
boolean cycleD(int u, List<List<Integer>> adj, int[] state) {
    state[u] = 1; // entering: this node is in-progress
    for (int v : adj.get(u)) {
        if (state[v] == 1) return true; // edge into a gray node = directed cycle
        if (state[v] == 0 && cycleD(v, adj, state)) return true;
        // state[v]==2: that branch is done and acyclic — ignore
    }
    state[u] = 2; // leaving: safe forever, other DFS trees may still start
    return false;
}
boolean hasCycleDirected(List<List<Integer>> adj, int n) {
    int[] state = new int[n];
    for (int i = 0; i < n; i++) {
        if (state[i] == 0 && cycleD(i, adj, state)) return true;
    }
    return false;
}
Example 1 — simple

Directed 2-cycle: 0→1 and 1→0. 3-state DFS from 0.

Gray = 1 = on the current path.
Callstate changeneighborresult
cycleD(0)0: unseen → gray1 is unseen → recursepending
cycleD(1)1: unseen → gray0 is graycycle → true

If you had used a single boolean, after finishing some other component you could still mis-read a finished node as a cycle. The gray state is the whole trick.

Example 2 — every step

Directed: 0→1, 1→2, 2→0, 2→3. The cycle is 0-1-2-0. 3 is a dangling sink.

0 → 1 → 2 → 3 ↑ │ └─────────┘ (2 → 0)
DFS from 0. Stop at the back-edge. Then note why 3 never matters.
Callnodestate before → after entryneighborresult
cycleD(0)00 → 1 (gray)1 unseen → recursepending
cycleD(1)10 → 12 unseen → recursepending
cycleD(2)20 → 10 is graycycle true
(not run)2’s other neighbor 3—never reachedthe function already returned

On an undirected copy of 0-1-2-0, the parent skip is required: from 1, neighbor 0 is visited but 0 is parent — not a cycle yet. From 2, neighbor 0 is visited and is not parent → cycle.

Time O(V+E) both versionsSpace O(V) color/visited + recursion
Why this

A boolean cannot distinguish “I am still exploring this node” from “I finished this node last week.” Directed cycle needs that distinction. Course Schedule is this algorithm plus emitting an order (next card).

Common mistakes
  • Using the undirected parent trick on a directed graph — you will miss 0→1→0 if you think 1’s edge to 0 is “just parent.”
  • Using only visited on directed graphs — an edge into a finished node looks like a cycle and you false-positive.
ProblemPatternLinks
Course Schedule I / IIdirected cycle + topoJava in ch.05 · cheat card · clue · clue · clue
Bipartite students / enemiesodd cycle vs 2-colorcheat card · clue · clue

Topological sort — Kahn and DFS postorder

Recognize it from

Prerequisites, build order, course schedule, task dependencies, alien dictionary, delivery stations + classes. Only valid on a DAG. If a cycle exists there is no order.

What it actually does

Kahn’s algorithm is BFS on indegrees. A node whose indegree is 0 has no unmet prerequisites — emit it, then “unlock” its neighbors by subtracting one from their indegree. If you cannot emit n nodes, the leftovers are on a cycle.

DFS topo is the same constraint in reverse: you may output a node only after every node it points to is already output. That is postorder, then reverse (or push onto a stack and pop).

Simple — read this first

// Kahn: start with indegree 0, peel those nodes, reduce neighbors
List<Integer> kahn(List<List<Integer>> adj, int n) {
    int[] indeg = new int[n];
    for (List<Integer> nbrs : adj) for (int v : nbrs) indeg[v]++;
    Queue<Integer> q = new ArrayDeque<>();
    for (int i = 0; i < n; i++) if (indeg[i] == 0) q.offer(i);
    List<Integer> order = new ArrayList<>();
    while (!q.isEmpty()) {
        int u = q.poll();
        order.add(u);
        for (int v : adj.get(u)) {
            if (--indeg[v] == 0) q.offer(v); // last remaining prereq just got done
        }
    }
    return order.size() == n ? order : List.of(); // leftover nodes = cycle
}

Interview — type this

// DFS topo: push a node AFTER every descendant is done (postorder), then reverse
void dfsTopo(int u, List<List<Integer>> adj, int[] state, Deque<Integer> st) {
    state[u] = 1;
    for (int v : adj.get(u)) {
        if (state[v] == 1) throw new IllegalStateException("cycle");
        if (state[v] == 0) dfsTopo(v, adj, state, st);
    }
    state[u] = 2;
    st.push(u); // postorder: dependents already in the stack under us
}
// pop the deque for the order. size < n or a throw means a cycle.
Example 1 — simple

Two courses: edge 0→1 means “0 before 1” (0 is a prereq of 1). Indegree [0, 1].

Kahn on 0→1.
Stepqueueemitindegree afterorder
init[0]—[0, 1][]
1poll 001 goes 1→0, offer 1[0]
2poll 11—[0, 1]

Order [0, 1]. If you also had 1→0, indegrees start at [1,1], queue is empty, order size 0 < 2 → cycle.

Example 2 — every step

Courses 0,1,2,3. Edges 0→1, 0→2, 1→3, 2→3 (“0 before 1 and 2; 1 and 2 before 3”).

0 / \ 1 2 \ / 3
Kahn. Indegree starts [0,1,1,2].
Steppollorderneighbors decrementednewly zero → queuequeue after
init—[]—0[0]
10[0]1: 1→0, 2: 1→01 and 2[1, 2]
21[0, 1]3: 2→1none yet (2 still 1)[2]
32[0, 1, 2]3: 1→03[3]
43[0, 1, 2, 3]——[]

Valid order 0, 1, 2, 3 (or 0, 2, 1, 3 if 2 was dequeued first). order.size()==4 so no cycle. Delivery-stations on LC 6369243 is this shape on that IE — do not stamp LC 210 on that post.

Time O(V+E)Space O(V)
Why this

The size check is the cycle detector. Skipping it silently returns a prefix of an order and looks like success.

Common mistakes
  • Returning the partial order when order.size() < n.
  • Stamping LC 210 onto delivery-stations / switching-list IEs. Course Schedule Java lives on the course-schedule card; 6369243 is a different pair of problems.
ProblemPatternLinks
Course Schedule II / directed cycleKahn or DFS colorsJava in ch.05 · cheat card · clue · clue
Delivery stations / parcels + classes + topotopo family, not LC 210cheat card · clue
Alien Dictionary (standard CS)build a graph from letter order, then Kahnclue

Union-Find (Disjoint Set)

Recognize it from

Connected components without a full DFS, “are these two in the same group,” redundant connection, number of provinces, Kruskal’s MST cycle check. Not the default for directed graphs.

What it actually does

Each node starts as its own set. find(x) returns the representative (root) of x’s set. union(a,b) merges the two sets if they differ. If they already share a root, the edge a–b is redundant — it would create a cycle in an undirected graph.

Path compression flattens the tree while you find. Union by rank hangs the smaller tree under the larger. Together, each operation is amortized almost O(1).

Simple — read this first

int[] parent;
int find(int x) {
    while (parent[x] != x) x = parent[x]; // walk up to the root — no compression yet
    return x;
}
void union(int a, int b) {
    int ra = find(a), rb = find(b);
    if (ra == rb) return; // already one set — this edge would close a cycle
    parent[rb] = ra; // hang b's tree under a's root
}

Interview — type this

int[] parent, rank;
int find(int x) {
    if (parent[x] != x) parent[x] = find(parent[x]); // path compression
    return parent[x];
}
void union(int a, int b) {
    int ra = find(a), rb = find(b);
    if (ra == rb) return;
    if (rank[ra] < rank[rb]) { int t = ra; ra = rb; rb = t; }
    parent[rb] = ra;
    if (rank[ra] == rank[rb]) rank[ra]++;
}
Example 1 — simple

Three nodes. union(0,1) then find(1) vs find(2).

No compression needed — trees are tiny.
opparent[]meaning
init[0, 1, 2]three sets
union(0,1)[0, 0, 2]1 hangs under 0
find(1)walk 1→0root 0
find(2)2 is rootroot 2 — different set

0 and 1 are connected. 2 is not. A later union(1,2) would merge 2 under 0.

Example 2 — every step

Six elements. union(0,1), union(1,2), union(3,4), union(2,3). Rank + compression as in the interview code.

parent[] after each union. Rank starts all 0.
opparent[]what happened
init[0,1,2,3,4,5]every node is a root
union(0,1)[0,0,2,3,4,5]equal rank → 1 under 0, rank[0] becomes 1
union(1,2)[0,0,0,3,4,5]find(1) is 0; rank[0]>rank[2] so 2 under 0
union(3,4)[0,0,0,3,3,5]4 under 3, rank[3] becomes 1
union(2,3)[0,0,0,0,3,5]find(2)=0 and find(3)=3, equal rank 1 → 3 under 0, rank[0] becomes 2

Sets: {0,1,2,3,4} and {5}. Calling find(4) now walks 4→3→0 and compresses parent[4]=0 (and parent[3]=0 on the way back).

Time α(n) amortized per op — ≤ 4 for any n that fits in memorySpace O(V)
Why this

α is inverse Ackermann. You say “almost constant” in the room. Path compression without rank is already fast; rank keeps the tree from going linear before you query it.

Common mistakes
  • Using Union-Find for a directed cycle — it does not track edge direction.
  • Forgetting that find must run on both sides before comparing roots (stale parent values).
ProblemPatternLinks
Union-Find / DSU Live Code cardparent + rank, then KruskalR4 coding
Redundant connection / provinces (standard CS)union until a same-set edgeclue · clue

Dijkstra — weighted shortest path, non-negative weights

Recognize it from

Shortest path when edges have weights ≥ 0. Network delay time, cheapest flights without a hop cap (hop cap is often Bellman-Ford / limited BFS).

What it actually does

Always settle the unsettled node whose current distance is smallest. Because remaining edges only add (never subtract), that distance can never improve later. A min-heap of (node, dist) candidates implements “who is closest.” Java’s PriorityQueue does not support decrease-key, so you push a new pair and skip stale pops with d > dist[u].

A negative edge breaks the “settled forever” claim. Then Bellman-Ford.

Simple — read this first

// Tiny version: scan for the unfinalized node with smallest dist each round (O(V²))
int[] dijkstraSlow(int[][] w, int src, int n) { // w[u][v] = weight or INF
    int[] dist = new int[n];
    boolean[] done = new boolean[n];
    Arrays.fill(dist, Integer.MAX_VALUE / 4);
    dist[src] = 0;
    for (int k = 0; k < n; k++) {
        int u = -1;
        for (int i = 0; i < n; i++) if (!done[i] && (u < 0 || dist[i] < dist[u])) u = i;
        done[u] = true;
        for (int v = 0; v < n; v++) dist[v] = Math.min(dist[v], dist[u] + w[u][v]);
    }
    return dist;
}

Interview — type this

int[] dijkstra(List<List<int[]>> adj, int src, int n) { // int[]{to, weight}
    int[] dist = new int[n];
    Arrays.fill(dist, Integer.MAX_VALUE / 4);
    dist[src] = 0;
    PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> Integer.compare(a[1], b[1]));
    pq.offer(new int[]{src, 0});
    while (!pq.isEmpty()) {
        int[] cur = pq.poll();
        int u = cur[0], d = cur[1];
        if (d > dist[u]) continue; // stale heap entry — ignore
        for (int[] e : adj.get(u)) {
            int v = e[0], w = e[1];
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                pq.offer(new int[]{v, dist[v]}); // leave the old worse pair in the heap
            }
        }
    }
    return dist;
}
Example 1 — simple

Three nodes. 0→1 weight 5, 0→2 weight 1, 2→1 weight 1. Source 0.

Heap pops. First time we settle 1 it should already be 2, not 5.
pop (u,d)stale?relaxdist[0,1,2]
(0,0)no1←5, 2←1[0, 5, 1]
(2,1)no1←1+1=2 < 5[0, 2, 1]
(1,2)nodone[0, 2, 1]
(1,5)yes 5>2skipunchanged

Answer [0, 2, 1]. The path 0→2→1 beat the direct edge.

Example 2 — every step

0→1 (4), 0→2 (1), 2→1 (2), 1→3 (1), 2→3 (5). Source 0.

Every heap pop, including stale ones.
pop (u,d)stale?relaxationsdist[0,1,2,3]heap after
(0,0)no1←4, 2←1[0,4,1,∞][(2,1),(1,4)]
(2,1)no1←3, 3←6[0,3,1,6][(1,3),(1,4),(3,6)]
(1,3)no3←4[0,3,1,4][(1,4),(3,4),(3,6)]
(1,4)yesskipunchanged[(3,4),(3,6)]
(3,4)nono outgoingunchanged[(3,6)]
(3,6)yesskipunchanged[]

Final [0, 3, 1, 4]. Cheapest Flights Within K Stops is not plain Dijkstra — the hop limit needs a modified Bellman-Ford or BFS with (node, hops).

Time O((V+E) log V) binary heapSpace O(V+E)
Why this

Negative weights: settle C too early and you miss a later A→B→C with a negative last edge. That is the textbook reason to switch algorithms, not a trivia fact.

Common mistakes
  • Using a[1]-b[1] as the heap comparator — overflow. Use Integer.compare.
  • Not skipping stale pops — you “re-settle” a node with a worse distance and can relax outgoing edges incorrectly if you also have a done[] flag set too late.
  • Running Dijkstra when the prompt allows negative weights or asks to detect a negative cycle.
ProblemPatternLinks
Dijkstra with PQ (IE-asked family)min-heap shortest pathcheat card
Similar to Cheapest Flights Within K StopsNOT plain Dijkstra — hop capcheat card

Bellman-Ford — negatives and negative-cycle check

Recognize it from

Negative weights allowed, or “detect a negative cycle,” or cheapest flights with at most K stops (relax K+1 rounds).

What it actually does

Relax every edge, V−1 times. A shortest simple path uses at most V−1 edges, so that many rounds are enough if there is no negative cycle. A V-th improving relaxation means a path with ≥ V edges got shorter — that path repeated a vertex, and the cycle’s total weight is negative.

Simple — read this first

// One extra round after V-1 is the negative-cycle check
int[] bf(int[][] edges, int n, int src) { // edges: u, v, w
    int[] dist = new int[n];
    Arrays.fill(dist, Integer.MAX_VALUE / 4);
    dist[src] = 0;
    for (int i = 0; i < n - 1; i++) {
        for (int[] e : edges) {
            int u = e[0], v = e[1], w = e[2];
            if (dist[u] + w < dist[v]) dist[v] = dist[u] + w;
        }
    }
    return dist;
}

Interview — type this

int[] bellmanFord(int[][] edges, int n, int src) {
    int[] dist = new int[n];
    Arrays.fill(dist, Integer.MAX_VALUE / 4);
    dist[src] = 0;
    for (int i = 0; i < n - 1; i++) {
        boolean any = false;
        for (int[] e : edges) {
            int u = e[0], v = e[1], w = e[2];
            if (dist[u] + w < dist[v]) { dist[v] = dist[u] + w; any = true; }
        }
        if (!any) break; // already stable
    }
    for (int[] e : edges) {
        int u = e[0], v = e[1], w = e[2];
        if (dist[u] + w < dist[v]) throw new IllegalStateException("neg cycle");
    }
    return dist;
}
Example 1 — simple

0→1 weight 4, 1→2 weight −2. Source 0. After round 1: dist [0,4,2]. Further rounds change nothing. No cycle.

One useful round.
roundrelax 0→1relax 1→2dist
10+4=44+(−2)=2[0, 4, 2]
2nonostable
Example 2 — every step

S=0→A=1 w4, S→B=2 w5, A→B w−2, B→C=3 w3, A→C w10.

Relax in the listed order each round. n=4 so 3 rounds scheduled.
roundS→AS→BA→BB→CA→Cdist[S,A,B,C]
1454−2=22+3=514 not <5[0,4,2,5]
2samesamesamesamesamestable — could stop
3samesamesamesamesame[0,4,2,5]
extrano improvement————no neg cycle

K-stop cheapest flights: you only run K+1 relaxation rounds (paths with at most K+1 edges), not necessarily V−1.

Time O(V·E)Space O(V)
Why this

V−1 is not a magic ritual. It is “longest simple path edge-count.” The extra round is the only honest negative-cycle test.

Common mistakes
  • Skipping the dist[u] finite check and overflowing on INF+negative.
  • Using Dijkstra because it is faster when the prompt clearly allows negatives.
ProblemPatternLinks
Similar to Cheapest Flights Within K Stopslimited relaxations / BFS with hopscheat card

Minimum spanning tree — Kruskal and Prim

Recognize it from

Connect all nodes with minimum total cost, min cost to connect all points. Kruskal is sort-edges + Union-Find. Prim is grow-a-tree + heap.

What it actually does

A spanning tree on V nodes has exactly V−1 edges and no cycle. Kruskal considers edges cheapest-first and skips any edge whose ends are already in the same set. Prim, from a start node, repeatedly adds the cheapest edge that reaches a new node.

Simple — read this first

// Kruskal needs Union-Find from the previous card
int kruskal(int[][] edges, int n) { // each edge [u,v,w]
    Arrays.sort(edges, (a, b) -> Integer.compare(a[2], b[2]));
    parent = new int[n];
    for (int i = 0; i < n; i++) parent[i] = i;
    int cost = 0, used = 0;
    for (int[] e : edges) {
        if (find(e[0]) == find(e[1])) continue; // would cycle
        union(e[0], e[1]);
        cost += e[2];
        if (++used == n - 1) break;
    }
    return cost;
}

Interview — type this

// Prim sketch: grow one tree, heap of edges that leave the tree
int prim(List<List<int[]>> adj, int n) {
    boolean[] in = new boolean[n];
    PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> Integer.compare(a[1], b[1]));
    pq.offer(new int[]{0, 0}); // node, cost to attach
    int cost = 0, taken = 0;
    while (!pq.isEmpty() && taken < n) {
        int[] cur = pq.poll();
        int u = cur[0], w = cur[1];
        if (in[u]) continue;
        in[u] = true;
        cost += w;
        taken++;
        for (int[] e : adj.get(u)) if (!in[e[0]]) pq.offer(new int[]{e[0], e[1]});
    }
    return taken == n ? cost : -1; // disconnected
}
Example 1 — simple

Three nodes. Edges (0,1,1), (1,2,2), (0,2,50). Kruskal takes 1, then 2, skips 50. Cost 3.

Sort already 1, 2, 50.
edgesame set?actioncost
(0,1,1)noaccept1
(1,2,2)noaccept — now V−1 edges3
(0,2,50)yes (all connected)stop / reject3
Example 2 — every step

Edges (0,1,10), (0,2,6), (0,3,5), (1,3,15), (2,3,4). Sorted: (2,3,4), (0,3,5), (0,2,6), (0,1,10), (1,3,15).

Kruskal with Union-Find from the previous card.
edgefind(u) vs find(v)actionrunning weight
(2,3,4)differentaccept, union4
(0,3,5)differentaccept, union9
(0,2,6)same — already linked via 3reject (cycle)9
(0,1,10)differentaccept, union — 3 edges = n−119
(1,3,15)samenot needed19

MST weight 19, edges (2,3), (0,3), (0,1).

Time Kruskal O(E log E) · Prim O(E log V) with a heapSpace O(V+E)
Why this

Kruskal if you are handed a flat edge list. Prim if you already have adj lists and the graph is denser. Union-Find is the cycle check that makes Kruskal a two-card story.

Common mistakes
  • Stopping when the edge list ends instead of when used == n-1 — a disconnected graph has no MST; return a sentinel.
  • Using Kruskal on a directed graph. MST is an undirected idea.
ProblemPatternLinks
Union-Find card (Kruskal’s engine)same DSU as aboveR4 coding