Coding Drills Β· 08

Graphs in Java

Nine drills that cover the graph algorithms interviews actually ask for: cloning with DFS, connectivity with Union-Find, dependency ordering with topological sort, shortest hops with BFS, and the two classic weighted algorithms β€” Kruskal's minimum spanning tree and Dijkstra's shortest paths. Every drill has a traced diagram, a complete Java program and the bugs to avoid.

9
Drills Β· 72–80
0
Easy
8
Medium
1
Hard
01
Chapter One Β· Core Idea

Nodes, Edges and the Adjacency List

A graph is a set of nodes (vertices) joined by edges. Edges may be directed (a prerequisite, a one-way street) or undirected (a friendship, a road), and weighted (distance, cost) or unweighted. Trees, linked lists and grids are all special cases β€” a tree is a connected graph with no cycles, a grid is a graph whose edges are implied by coordinates. The drills on this page are the general case, where you must build the edges yourself from an input list.

In Java the standard representation is an adjacency list: List<List<Integer>> graph, where graph.get(u) holds every neighbour of u. It uses O(V + E) memory and lets you iterate a node's neighbours in time proportional to its degree, which is what makes every traversal below O(V + E). An adjacency matrix (boolean[V][V]) answers "is there an edge u–v?" in O(1) but costs O(VΒ²) memory and O(V) per neighbour scan, so it only pays off for small dense graphs.

The same graph as a picture and as an adjacency list
UNDIRECTED GRAPH (V = 6, E = 6) 0 1 2 3 4 5 ADJACENCY LIST 0 1 2 1 0 3 2 0 3 3 1 2 4 4 3 5 5 4 graph.get(u) = neighbours of u each undirected edge is stored twice (u→v and v→u) · memory O(V + E)
Analogy: an adjacency list is a phone's contact list per person. To find everyone reachable from you, call your contacts, then their contacts, ticking off names you've already called so you never call anyone twice. That tick-list is the visited set, and it is the single most important line in every graph algorithm β€” without it, any cycle turns the search into an infinite loop.
Java β€” adjacency list
// Build an undirected adjacency list from an edge list β€” the first 4 lines of most graph drills. List<List<Integer>> graph = new ArrayList<>(); for (int i = 0; i < n; i++) graph.add(new ArrayList<>()); for (int[] e : edges) { graph.get(e[0]).add(e[1]); graph.get(e[1]).add(e[0]); // omit this line for a directed graph }
πŸ”

Traversal

DFS (recursion / stack) or BFS (queue) visits every reachable node once: O(V + E). BFS also gives fewest-edge distances.

πŸ”—

Connectivity

Union-Find answers "are u and v in the same group?" in near-O(1) amortised and merges groups as edges arrive.

βš–οΈ

Weights

Positive weights need a priority queue: Dijkstra for one-source shortest paths, Kruskal (sort + Union-Find) for the cheapest spanning tree.

02
Chapter Two Β· Mental Model

Turn the Story into Nodes and Edges

Graph questions rarely say "graph". They say courses and prerequisites, accounts sharing an email, words differing by one letter, cities and flight costs. Step one is always to name the nodes and the edges; step two is to recognise what is being asked of them.

Nodes can be reached by more than one path (cycles, shared neighbours)
β–Ά
A visited set (or map) is mandatory β€” it makes every traversal O(V + E) and prevents infinite loops
BFS pops nodes in order of edge count from the source
β–Ά
In an unweighted graph the first time BFS reaches a node is a shortest path (Word Ladder)
Union-Find keeps one representative (root) per connected group
β–Ά
β€œSame group?” is find(a) == find(b); adding an edge inside one group means that edge closes a cycle
A directed graph has a valid ordering only if it has no cycle
β–Ά
Topological sort and cycle detection are the same algorithm β€” Kahn's BFS fails to output every node exactly when a cycle exists
With weights, fewer edges does not mean cheaper
β–Ά
Replace the FIFO queue with a min-priority queue keyed by distance β€” that is Dijkstra
The cheapest edge that joins two different components is always safe to take
β–Ά
Kruskal's greedy: sort edges by weight, keep each one that Union-Find says connects new components
What does the question ask? Copy or visit everything reachable? yes DFS / BFS + map Drill 72 Which nodes are connected? merge into groups? yes Union-Find 73 Β· 74 Β· 77 Order tasks with prerequisites, or detect a cycle? yes Topological sort 75 Β· 76 Fewest hops when every edge costs the same? yes BFS by levels Drill 78 Cheapest edge set that links every node together? yes Kruskal MST Drill 79 Cheapest path from one source, all weights β‰₯ 0? yes Dijkstra Drill 80 Weighted edges rule out plain BFS Β· negative weights rule out Dijkstra (use Bellman-Ford)
03
Chapter Three Β· Pattern Toolkit

Four Templates for Every Graph

Four templates, each a few lines long, are the building blocks of all nine drills. They are written in pseudocode here; every drill below has the full Java version.

Template 1 β€” Traversal with a visited set (DFS / BFS)
Visit every node reachable from a source exactly once. BFS additionally yields distances in edges.
Pseudocode
function bfs(graph, src): visited = {src}; queue = [src]; dist = 0 while queue not empty: repeat queue.size() times: // one layer u = queue.poll() for each v in graph[u]: if v not in visited: visited.add(v) // mark on ENQUEUE queue.offer(v) dist = dist + 1 function dfs(u): visited.add(u) for each v in graph[u]: if v not in visited: dfs(v)
BFS layers on the chapter-one graph
BFS FROM 0 β€” colour = layer (edges from source) 0 d=0 1 d=1 2 d=1 3 d=2 4 d=3 5 d=4 QUEUE CONTENTS PER LAYER L0 0 L1 1 2 L2 3 L3 4 L4 5 visited is set when a node is ENQUEUED, so node 3 (neighbour of 1 and 2) enters once
Template 2 β€” Union-Find (Disjoint Set Union)
Maintain groups under "merge" operations. Path compression + union by rank make each operation O(Ξ±(n)) amortised β€” effectively constant.
Pseudocode
parent[i] = i, rank[i] = 0 for every i function find(x): if parent[x] != x: parent[x] = find(parent[x]) // path compression return parent[x] function union(a, b): ra = find(a), rb = find(b) if ra == rb: return false // already together β†’ edge closes a cycle if rank[ra] < rank[rb]: swap(ra, rb) parent[rb] = ra // shorter tree under taller if rank[ra] == rank[rb]: rank[ra] = rank[ra] + 1 return true
The two optimisations that make Union-Find fast
1 Β· find(3) BEFORE 0 1 2 3 parent[3]=2 parent[2]=1 parent[1]=0 3 hops to root 2 Β· AFTER PATH COMPRESSION 0 1 2 3 every node on the path now points at the root 3 Β· UNION BY RANK 0 rank 1 1 5 rank 0 union(1, 5): parent[5] = 0 shorter tree goes under taller
Template 3 β€” Kahn's topological sort (BFS on in-degrees)
Repeatedly emit a node with no remaining prerequisites. If fewer than V nodes come out, the graph has a cycle.
Pseudocode
indegree[v] = number of edges u -> v queue = every node with indegree 0 order = [] while queue not empty: u = queue.poll(); order.add(u) for each v in graph[u]: indegree[v] = indegree[v] - 1 if indegree[v] == 0: queue.offer(v) return order.size() == V ? order : CYCLE
Kahn's algorithm peels nodes whose in-degree has dropped to zero
A in=0 B in=1 C in=1 D in=2 queue = [A] in-degree 0 at start pop A β†’ B:0, C:0 queue = [B, C] pop B β†’ D:1 queue = [C] pop C β†’ D:0 queue = [D] pop D order = A B C D βœ“ a node is emitted only after ALL of its prerequisites have been emitted
Template 4 β€” Dijkstra (greedy with a min-heap)
Single-source shortest paths with non-negative weights. Always settle the closest unsettled node next.
Pseudocode
dist[*] = INF; dist[src] = 0 pq = min-heap of (node, dist), start with (src, 0) while pq not empty: (u, d) = pq.poll() if d > dist[u]: continue // stale entry, u already settled cheaper for each (v, w) in graph[u]: if d + w < dist[v]: dist[v] = d + w // relax pq.offer((v, dist[v]))
Relaxation: a longer route with smaller weights can beat a direct edge
1 4 2 S d=0 A d=1 B d: 4 β†’ 3 pop S (0) dist A = 1, dist B = 4 pop A (1) relax Aβ†’B: 1+2 = 3 < 4 β†’ B = 3 pop B (3) stale entry (B,4) is skipped later the node with the smallest tentative distance can never be improved later β€” if all weights β‰₯ 0
TemplateAnswersTimeSpaceDrills
DFS / BFS + visitedreachability, copy, unweighted distanceO(V + E)O(V)72, 78
Union-Findconnectivity, cycle in undirected graph, groupingO(E Β· Ξ±(V))O(V)73, 74, 77, 79
Kahn / 3-colour DFSdependency order, cycle in directed graphO(V + E)O(V + E)75, 76
Dijkstraweighted shortest paths, w β‰₯ 0O((V + E) log V)O(V + E)80
04
Drill 72 Β· Graph DFS + HashMap

Clone Graph

🏷️

Difficulty

Medium

πŸ”—

LeetCode

LC 133 Β· Clone Graph

🧩

Pattern

DFS with an original β†’ copy map

graph dfs hashmap deep copy

Problem

Given a reference to one node of a connected undirected graph, return a deep copy of the whole graph. Each node has an integer val and a list of neighbors. The copy must contain brand-new node objects with the same values and the same neighbour structure β€” no reference may point back into the original graph.

Example
Input: adjList = [[2,4],[1,3],[2,4],[1,3]] // node 1's neighbours are 2 and 4, etc. Output: [[2,4],[1,3],[2,4],[1,3]] // same shape, all-new objects
Key insight: a HashMap<Node, Node> from original to copy is both the visited set and the lookup table for wiring edges. The copy must be put in the map before recursing into neighbours β€” that is what lets a cycle (1 β†’ 2 β†’ 3 β†’ 4 β†’ 1) find the half-built copy of node 1 instead of cloning it again forever.
From brute force to optimal

A naive recursive copy ("new node, then copy each neighbour") never terminates on a cycle, and on a DAG-shaped graph it duplicates shared nodes. Tracking visited originals fixes termination, but you still need to find the copy of an already-visited neighbour to wire the edge β€” hence a map rather than a set. DFS or BFS both work in O(V + E).

Algorithm
  • If the input is null, return null.
  • clone(node): if the map has it, return the stored copy.
  • Otherwise create the copy, put it in the map, then for each neighbour add clone(neighbour) to the copy's list.
  • Return the copy of the start node.
Walkthrough
Original vs clone β€” same shape, new objects
ORIGINAL 1 2 3 4 CLONE (creation order in gold) 1' #1 2' #2 3' #3 4' #4 map (original β†’ copy) node1 β†’ node1' node2 β†’ node2' node3 β†’ node3' node4 β†’ node4' copy registered BEFORE its neighbours are cloned, so the cycle 1-2-3-4-1 hits the map instead of recursing forever
StepCallMap afterResult
1clone(1){1}create 1', recurse into neighbour 2
2clone(2){1, 2}neighbour 1 is in map β†’ link 2'β†’1'; recurse into 3
3clone(3){1, 2, 3}neighbour 2 in map β†’ link; recurse into 4
4clone(4){1, 2, 3, 4}neighbours 1 and 3 both in map β†’ link both, return 4'
5back in clone(1)unchangedneighbour 4 in map β†’ link 1'β†’4'; return 1'
Java β€” CloneGraph.java (complete, runnable)
import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class CloneGraph { static class Node { int val; List<Node> neighbors = new ArrayList<>(); Node(int val) { this.val = val; } } public static Node cloneGraph(Node node) { if (node == null) return null; return clone(node, new HashMap<>()); } private static Node clone(Node node, Map<Node, Node> copies) { Node existing = copies.get(node); if (existing != null) return existing; // already cloned (handles cycles) Node copy = new Node(node.val); copies.put(node, copy); // register BEFORE recursing for (Node nb : node.neighbors) copy.neighbors.add(clone(nb, copies)); return copy; } public static void main(String[] args) { Node n1 = new Node(1), n2 = new Node(2), n3 = new Node(3), n4 = new Node(4); n1.neighbors.addAll(List.of(n2, n4)); n2.neighbors.addAll(List.of(n1, n3)); n3.neighbors.addAll(List.of(n2, n4)); n4.neighbors.addAll(List.of(n1, n3)); Node c1 = cloneGraph(n1); Node c2 = c1.neighbors.get(0); System.out.println(c1.val + " -> " + c1.neighbors.get(0).val + ", " + c1.neighbors.get(1).val); // 1 -> 2, 4 System.out.println(c1 != n1 && c2 != n2); // true (new objects) System.out.println(c2.neighbors.get(0) == c1); // true (cycle preserved inside the copy) } }
Complexity
ApproachTimeSpaceNotes
Naive recursion (no map)∞ on cyclesβ€”Never terminates
DFS / BFS + HashMapO(V + E)O(V)Map holds V copies; recursion depth up to V
⚠ Common bugs & edge cases:
  • Putting the copy into the map after the neighbour loop β€” the cycle recurses back before the entry exists and overflows the stack.
  • Returning the original neighbour instead of its clone (copy.neighbors.add(nb)) β€” the result is a shallow copy that shares nodes with the input.
  • Keying the map by val is fine only when values are unique; keying by the node object is always safe.
  • Forgetting null input β€” LeetCode tests the empty graph.
  • The original drill only printed the root value, which cannot detect a shallow copy β€” this version also checks object identity and cycle preservation.
05
Drill 73 Β· Union-Find

Number of Connected Components

🏷️

Difficulty

Medium

🧩

Pattern

Union-Find with a component counter

graph union-find connected components

Problem

You are given n nodes labelled 0 to n - 1 and a list of undirected edges. Return the number of connected components β€” maximal groups of nodes where every node can reach every other.

Example
Input: n = 5, edges = [[0,1],[1,2],[3,4]] Output: 2 // {0,1,2} and {3,4}
Key insight: start with n singleton components. Every edge that joins two different components reduces the count by exactly one; an edge inside one component changes nothing. So the answer is n βˆ’ (number of successful unions), and Union-Find tells you in near-constant time whether a union succeeds.
From brute force to optimal

Building an adjacency list and launching a DFS from every unvisited node also works in O(V + E) and is a perfectly good answer. Union-Find avoids building the adjacency list at all, processes edges in a single streaming pass, and is the better fit when edges arrive over time ("how many components after each new edge?"). Without path compression and union by rank, chains can grow to length n and each find costs O(n), giving O(nΒ·E).

Algorithm
  • Initialise parent[i] = i, rank[i] = 0, components = n.
  • For each edge (a, b), call union(a, b); on success, components--.
  • Return components.
Walkthrough
Graph and its Union-Find forest
GRAPH n = 5, edges (0,1) (1,2) (3,4) 0 1 2 3 4 2 components β†’ answer 2 UNION-FIND FOREST AFTER ALL UNIONS 0 root Β· rank 1 1 2 3 root Β· rank 1 4 components = 5 βˆ’ 3 successful unions = 2
Edgefind(a), find(b)Actionparent[]components
startβ€”β€”[0, 1, 2, 3, 4]5
(0,1)0, 1equal ranks β†’ parent[1]=0, rank[0]=1[0, 0, 2, 3, 4]4
(1,2)0, 2rank 1 > 0 β†’ parent[2]=0[0, 0, 0, 3, 4]3
(3,4)3, 4equal ranks β†’ parent[4]=3, rank[3]=1[0, 0, 0, 3, 3]2
Java β€” ConnectedComponents.java (complete, runnable)
public class ConnectedComponents { static class UnionFind { private final int[] parent, rank; int components; UnionFind(int n) { parent = new int[n]; rank = new int[n]; components = n; for (int i = 0; i < n; i++) parent[i] = i; } int find(int x) { if (parent[x] != x) parent[x] = find(parent[x]); // path compression return parent[x]; } boolean union(int a, int b) { int ra = find(a), rb = find(b); if (ra == rb) return false; // already connected if (rank[ra] < rank[rb]) { int t = ra; ra = rb; rb = t; } parent[rb] = ra; // attach shorter tree under taller if (rank[ra] == rank[rb]) rank[ra]++; components--; return true; } } public static int countComponents(int n, int[][] edges) { UnionFind uf = new UnionFind(n); for (int[] e : edges) uf.union(e[0], e[1]); return uf.components; } public static void main(String[] args) { int[][] edges = {{0, 1}, {1, 2}, {3, 4}}; System.out.println(countComponents(5, edges)); // 2 System.out.println(countComponents(4, new int[0][])); // 4 (no edges: every node alone) } }
Complexity
ApproachTimeSpaceNotes
DFS from each unvisited nodeO(V + E)O(V + E)Needs the adjacency list
Union-Find, no optimisationsO(V Β· E)O(V)Degenerate chains
Union-Find, rank + compressionO(V + E Β· Ξ±(V))O(V)Ξ± ≀ 4 for any realistic V
⚠ Common bugs & edge cases:
  • Decrementing the counter on every edge, including edges inside an existing component (e.g. a triangle would drop the count too far).
  • Writing parent[a] = b instead of parent[find(a)] = find(b) β€” links a non-root and silently splits a tree.
  • Isolated nodes (no edges) are components too: n = 4, edges = [] must return 4.
  • The original drill's complexity claim O(EΒ·Ξ±(N)) ignored the O(N) initialisation β€” the total is O(N + EΒ·Ξ±(N)).
06
Drill 74 Β· Union-Find Β· Cycle Detection

Graph Valid Tree

🏷️

Difficulty

Medium

🧩

Pattern

Edge count + Union-Find cycle check

graph union-find tree property

Problem

Given n nodes labelled 0 to n - 1 and a list of undirected edges, return true if the edges form a valid tree β€” that is, the graph is connected and has no cycles.

Example
Input: n = 5, edges = [[0,1],[0,2],[0,3],[1,4]] Output: true Input: n = 5, edges = [[0,1],[1,2],[2,0],[3,4]] Output: false // 0-1-2 is a cycle; {3,4} is cut off
Key insight: a graph on n nodes is a tree iff it has exactly n βˆ’ 1 edges and no cycle (equivalently: n βˆ’ 1 edges and connected). Check the edge count first β€” O(1) β€” then use Union-Find: an edge whose endpoints already share a root closes a cycle. If all n βˆ’ 1 unions succeed, the components dropped from n to 1, so the graph is automatically connected.
From brute force to optimal

The DFS alternative builds an adjacency list, runs DFS from node 0 while tracking the parent (to ignore the edge you came from), fails on any visited non-parent neighbour, and finally checks that all n nodes were visited. It is O(V + E) but has two separate failure modes to get right. The edge-count + Union-Find version has one loop and no parent bookkeeping.

Algorithm
  • If edges.length != n - 1, return false.
  • For each edge, if union(a, b) fails (same root), return false.
  • Return true.
Walkthrough
A valid tree vs. a graph with a cycle
n=5, edges (0,1)(0,2)(0,3)(1,4) 0 1 2 3 4 4 edges = nβˆ’1 βœ“ every union succeeds βœ“ β†’ true n=5, edges (0,1)(1,2)(2,0)(3,4) 0 1 2 3 4 edge (2,0): find(2) = find(0) = 0 β†’ cycle β†’ false (and {3,4} is disconnected)
InputEdgeRootsVerdict
valid(0,1) (0,2) (0,3) (1,4)always different4 successful unions β†’ true
cyclic(0,1)0, 1union β†’ parent[1]=0
cyclic(1,2)0, 2union β†’ parent[2]=0
cyclic(2,0)0, 0same root β†’ return false
Java β€” GraphValidTree.java (complete, runnable)
public class GraphValidTree { private static int[] parent, rank; public static boolean validTree(int n, int[][] edges) { if (edges.length != n - 1) return false; // a tree on n nodes has exactly n-1 edges parent = new int[n]; rank = new int[n]; for (int i = 0; i < n; i++) parent[i] = i; for (int[] e : edges) { if (!union(e[0], e[1])) return false; // edge inside one component = cycle } return true; // n-1 edges + no cycle => connected } private static int find(int x) { if (parent[x] != x) parent[x] = find(parent[x]); return parent[x]; } private static boolean union(int a, int b) { int ra = find(a), rb = find(b); if (ra == rb) return false; if (rank[ra] < rank[rb]) { int t = ra; ra = rb; rb = t; } parent[rb] = ra; if (rank[ra] == rank[rb]) rank[ra]++; return true; } public static void main(String[] args) { System.out.println(validTree(5, new int[][]{{0, 1}, {0, 2}, {0, 3}, {1, 4}})); // true System.out.println(validTree(5, new int[][]{{0, 1}, {1, 2}, {2, 0}, {3, 4}})); // false (cycle 0-1-2) } }
Complexity
ApproachTimeSpaceNotes
DFS with parent trackingO(V + E)O(V + E)Two checks: no back edge, all visited
Edge count + Union-FindO(V + E Β· Ξ±(V))O(V)Early exit on wrong edge count
⚠ Common bugs & edge cases:
  • Checking only "no cycle" β€” a forest (two separate trees) has no cycle but is not a tree. The n βˆ’ 1 edge check catches it.
  • Checking only edges == n βˆ’ 1 β€” the cyclic example above has 4 edges for 5 nodes and still fails.
  • In the DFS version, treating the edge back to the parent as a cycle (every undirected edge looks like a 2-cycle).
  • n = 1, edges = [] is a valid tree (0 = n βˆ’ 1 edges).
07
Drill 75 Β· Directed Cycle Detection

Course Schedule

🏷️

Difficulty

Medium

🧩

Pattern

3-colour DFS (white / gray / black)

graph dfs cycle detection directed

Problem

There are numCourses courses labelled 0 to numCourses βˆ’ 1. Each pair [a, b] in prerequisites means you must take course b before course a. Return true if it is possible to finish every course.

Example
Input: numCourses = 2, prerequisites = [[1,0]] Output: true // take 0, then 1 Input: numCourses = 4, prerequisites = [[1,0],[2,1],[3,2],[1,3]] Output: false // 1 β†’ 2 β†’ 3 β†’ 1 is a cycle
Key insight: all courses can be finished iff the prerequisite graph has no directed cycle. A plain visited flag is not enough to detect one in a directed graph β€” reaching an already-visited node may just mean two paths converge (a diamond). What signals a cycle is reaching a node that is still on the current DFS path. Three colours capture this: WHITE (never seen), GRAY (on the path right now), BLACK (fully explored and proven cycle-free).
From brute force to optimal

Trying every ordering of courses is O(V!) and hopeless. Two O(V + E) answers exist: Kahn's algorithm (count how many nodes can be peeled with in-degree 0 β€” see the next drill) and DFS colouring, shown here. The BLACK state is the key optimisation: a node proven cycle-free is never re-explored, so each node and edge is processed once. Without it (only an on-path flag) the DFS revisits shared sub-graphs and can become exponential.

Algorithm
  • Build the adjacency list with edges b β†’ a for each pair [a, b].
  • For every WHITE course, run hasCycle.
  • hasCycle(u): colour GRAY; for each neighbour, GRAY means cycle; WHITE means recurse; then colour BLACK.
  • Return false if any cycle is found, otherwise true.
Walkthrough
The back edge 3 β†’ 1 points at a GRAY node
edges prereq β†’ course: 0β†’1, 1β†’2, 2β†’3, 3β†’1 0 GRAY 1 GRAY 2 GRAY 3 GRAY back edge DFS STACK WHEN THE CYCLE IS FOUND hasCycle(0) hasCycle(1) hasCycle(2) hasCycle(3) 3 β†’ 1: 1 is GRAY (still on stack) β†’ return true WHITE = unvisited Β· GRAY = on path Β· BLACK = done
CallColours (0 1 2 3)Event
hasCycle(0)G W W Wneighbour 1 is WHITE β†’ recurse
hasCycle(1)G G W Wneighbour 2 is WHITE β†’ recurse
hasCycle(2)G G G Wneighbour 3 is WHITE β†’ recurse
hasCycle(3)G G G Gneighbour 1 is GRAY β†’ cycle β†’ true propagates up β†’ canFinish returns false
Java β€” CourseSchedule.java (complete, runnable)
import java.util.ArrayList; import java.util.List; public class CourseSchedule { private static final int WHITE = 0, GRAY = 1, BLACK = 2; // unvisited, on stack, done public static boolean canFinish(int numCourses, int[][] prerequisites) { List<List<Integer>> graph = new ArrayList<>(); for (int i = 0; i < numCourses; i++) graph.add(new ArrayList<>()); for (int[] p : prerequisites) graph.get(p[1]).add(p[0]); // edge: prereq -> course int[] state = new int[numCourses]; for (int c = 0; c < numCourses; c++) { if (state[c] == WHITE && hasCycle(c, graph, state)) return false; } return true; } private static boolean hasCycle(int u, List<List<Integer>> graph, int[] state) { state[u] = GRAY; // u is on the current DFS path for (int v : graph.get(u)) { if (state[v] == GRAY) return true; // back edge -> cycle if (state[v] == WHITE && hasCycle(v, graph, state)) return true; } state[u] = BLACK; // fully explored, provably cycle-free return false; } public static void main(String[] args) { System.out.println(canFinish(2, new int[][]{{1, 0}})); // true System.out.println(canFinish(4, new int[][]{{1, 0}, {2, 1}, {3, 2}, {1, 3}})); // false } }
Edge direction: [a, b] means "b before a", so the edge is b β†’ a. Reversing every edge does not change whether a cycle exists, so Course Schedule passes either way β€” but Course Schedule II (the ordering) does depend on it. Pick the direction deliberately.
Complexity
ApproachTimeSpaceNotes
Try all orderingsO(V!)O(V)Infeasible
DFS with on-path flag onlyexponential worst caseO(V)Re-explores shared sub-DAGs
3-colour DFSO(V + E)O(V + E)Recursion depth up to V
⚠ Common bugs & edge cases:
  • Using a single visited flag and calling every visited neighbour a cycle β€” a diamond 0β†’1, 0β†’2, 1β†’3, 2β†’3 would be falsely rejected.
  • Forgetting to colour BLACK on the way out (or resetting to WHITE) β€” correctness holds but performance collapses on dense DAGs.
  • Starting DFS only from course 0 β€” the graph may be disconnected; loop over every course.
  • Very deep chains (10⁡ courses in a line) can overflow the Java stack; Kahn's iterative BFS avoids recursion entirely.
08
Drill 76 Β· Kahn's Algorithm

Topological Sort

🏷️

Difficulty

Medium

πŸ”—

LeetCode

No exact LeetCode match β€” closest: LC 210 Β· Course Schedule II

🧩

Pattern

BFS on in-degrees (Template 3)

graph bfs topological order dag

Problem

Given n nodes and a list of directed edges [u, v] meaning "u must come before v", return an ordering of all nodes that respects every edge (a topological order). If the graph contains a cycle, no such order exists β€” return an empty array.

Example
Input: n = 6, edges = [[5,2],[5,0],[4,0],[4,1],[2,3],[3,1]] Output: [4, 5, 2, 0, 3, 1] Input: n = 2, edges = [[0,1],[1,0]] Output: [] // cycle
Key insight: a node can go next as soon as all of its incoming edges come from nodes already placed β€” i.e. its remaining in-degree is 0. Keep a queue of such nodes; each time you output one, "delete" its outgoing edges by decrementing its neighbours' in-degrees and enqueue any that hit 0. Nodes on a cycle never reach in-degree 0, so if the output is shorter than n, there is a cycle.
From brute force to optimal

Generating permutations and checking each is O(n!Β·E). The DFS alternative pushes each node onto a stack after its descendants finish and reverses the stack at the end β€” also O(V + E), but it needs separate cycle detection (the gray state from Drill 75). Kahn's algorithm gives the order and the cycle check together, iteratively, with no recursion depth risk.

Algorithm
  • Build the adjacency list and indegree[].
  • Enqueue every node with in-degree 0 (in index order here: 4, 5).
  • Poll u, append to the order, decrement each neighbour's in-degree, enqueue those that reach 0.
  • If the order has n nodes return it; otherwise return [].
Walkthrough
The DAG with output positions
5 #2 Β· in=0 4 #1 Β· in=0 2 #3 Β· in=1 0 #6 Β· in=2 3 #4 Β· in=1 1 #5 Β· in=2 label: position in output Β· initial in-degree OUTPUT 4 #1 5 #2 2 #3 0 #4 3 #5 1 #6
PollDecrementNewly zero β†’ enqueueQueue afterin-degree [0..5]
startβ€”4, 5[4, 5][2, 2, 1, 1, 0, 0]
40: 2β†’1, 1: 2β†’1β€”[5][1, 1, 1, 1, 0, 0]
52: 1β†’0, 0: 1β†’02, 0[2, 0][0, 1, 0, 1, 0, 0]
23: 1β†’03[0, 3][0, 1, 0, 0, 0, 0]
0(no out-edges)β€”[3]unchanged
31: 1β†’01[1][0, 0, 0, 0, 0, 0]
1(no out-edges)β€”[]6 nodes output β†’ valid
Java β€” TopologicalSort.java (complete, runnable)
import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Queue; public class TopologicalSort { // edges[i] = {u, v} means "u must come before v". Returns [] if the graph has a cycle. public static int[] topoSort(int n, int[][] edges) { List<List<Integer>> graph = new ArrayList<>(); for (int i = 0; i < n; i++) graph.add(new ArrayList<>()); int[] indegree = new int[n]; for (int[] e : edges) { graph.get(e[0]).add(e[1]); indegree[e[1]]++; } Queue<Integer> queue = new ArrayDeque<>(); for (int i = 0; i < n; i++) if (indegree[i] == 0) queue.offer(i); int[] order = new int[n]; int idx = 0; while (!queue.isEmpty()) { int u = queue.poll(); order[idx++] = u; for (int v : graph.get(u)) { if (--indegree[v] == 0) queue.offer(v); // last prerequisite satisfied } } return idx == n ? order : new int[0]; // fewer than n => cycle } public static void main(String[] args) { int[][] edges = {{5, 2}, {5, 0}, {4, 0}, {4, 1}, {2, 3}, {3, 1}}; System.out.println(Arrays.toString(topoSort(6, edges))); // [4, 5, 2, 0, 3, 1] System.out.println(Arrays.toString(topoSort(2, new int[][]{{0, 1}, {1, 0}}))); // [] (cycle) } }
Complexity
ApproachTimeSpaceNotes
Try all permutationsO(n! Β· E)O(n)Infeasible
DFS post-order + reverseO(V + E)O(V + E)Needs a separate cycle check
Kahn's algorithmO(V + E)O(V + E)Order + cycle detection in one pass
⚠ Common bugs & edge cases:
  • The original drill used DFS with java.util.Stack and claimed output [0,1,2,3]; the code actually returns [0, 2, 1, 3] (also valid). Topological orders are generally not unique β€” never hard-code one expected answer in a test without saying which algorithm/tie-break produced it.
  • The original also had no cycle check at all and would return a meaningless order for a cyclic graph.
  • Mixing up edge direction ([u, v] vs [v, u]) reverses the output. State the convention explicitly.
  • Decrementing in-degree when a node is enqueued instead of when its predecessor is polled.
09
Drill 77 Β· Union-Find Β· Grouping

Accounts Merge

🏷️

Difficulty

Medium

🧩

Pattern

Union-Find over account indices

graph union-find hashmap sorting

Problem

Each account is a list whose first element is a name and the rest are email addresses. Two accounts belong to the same person if they share at least one email (directly or through a chain of accounts). Accounts with the same name may belong to different people. Merge the accounts and return each person as [name, emails in sorted order].

Example
Input: [["John","johnsmith@mail.com","john_newyork@mail.com"], ["John","johnsmith@mail.com","john00@mail.com"], ["Mary","mary@mail.com"], ["John","johnnybravo@mail.com"]] Output: [["John","john00@mail.com","john_newyork@mail.com","johnsmith@mail.com"], ["Mary","mary@mail.com"], ["John","johnnybravo@mail.com"]]
Key insight: this is connected components where the nodes are accounts and an edge exists whenever two accounts share an email. You don't need to build those edges explicitly: walk the emails with a map email β†’ first account that listed it; when an email is seen again, union the current account with that owner. Afterwards, every email belongs to the root of its owner's set.
From brute force to optimal

Comparing every pair of accounts for shared emails is O(AΒ² Β· k) and still needs transitive merging. A graph-of-emails DFS (connect all emails within an account, then DFS each component) is the other standard answer, O(N log N). Union-Find over account indices is the most compact: one pass to union, one pass to group, then sort each group. Sorting dominates: O(N log N) for N total emails.

Algorithm
  • parent[i] = i for each account.
  • For each email of account i: owner.putIfAbsent(email, i); if it already had an owner p, union i with p.
  • For each (email, owner), add the email to a TreeSet keyed by find(owner).
  • Emit [name of root account] + sorted emails for each root.
Walkthrough
Accounts linked through the shared email johnsmith@mail.com
ACCOUNTS EMAILS (edge = account lists email) A0 John A1 John A2 Mary A3 John john_newyork@mail.com johnsmith@mail.com john00@mail.com mary@mail.com johnnybravo@mail.com shared RESULT (one row per root) root A0 β†’ John john00@mail.com john_newyork@mail.com johnsmith@mail.com root A2 β†’ Mary mary@mail.com root A3 β†’ John johnnybravo@mail.com A1 sees johnsmith already owned by A0 β†’ union(A1, A0); same name β‰  same person (A3)
AccountEmailOwner mapUnion
A0johnsmith, john_newyorkboth β†’ A0β€”
A1johnsmithalready A0union(A1, A0): parent[A1] = A0
A1john00β†’ A1β€”
A2mary→ A2—
A3johnnybravo→ A3— (same name as A0, but no shared email)
Java β€” AccountsMerge.java (complete, runnable)
import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.TreeSet; public class AccountsMerge { private static int[] parent; private static int find(int x) { while (parent[x] != x) { parent[x] = parent[parent[x]]; // path halving x = parent[x]; } return x; } public static List<List<String>> accountsMerge(List<List<String>> accounts) { int n = accounts.size(); parent = new int[n]; for (int i = 0; i < n; i++) parent[i] = i; // 1. Union accounts that share an email. Map<String, Integer> owner = new HashMap<>(); // email -> first account index for (int i = 0; i < n; i++) { List<String> acc = accounts.get(i); for (int j = 1; j < acc.size(); j++) { Integer prev = owner.putIfAbsent(acc.get(j), i); if (prev != null) parent[find(i)] = find(prev); } } // 2. Group every email under its account's root (sorted). Map<Integer, TreeSet<String>> groups = new TreeMap<>(); for (Map.Entry<String, Integer> e : owner.entrySet()) { groups.computeIfAbsent(find(e.getValue()), k -> new TreeSet<>()).add(e.getKey()); } // 3. Emit [name, sorted emails...] per group. List<List<String>> result = new ArrayList<>(); for (Map.Entry<Integer, TreeSet<String>> g : groups.entrySet()) { List<String> merged = new ArrayList<>(); merged.add(accounts.get(g.getKey()).get(0)); merged.addAll(g.getValue()); result.add(merged); } return result; } public static void main(String[] args) { List<List<String>> accounts = List.of( List.of("John", "johnsmith@mail.com", "john_newyork@mail.com"), List.of("John", "johnsmith@mail.com", "john00@mail.com"), List.of("Mary", "mary@mail.com"), List.of("John", "johnnybravo@mail.com") ); for (List<String> acc : accountsMerge(accounts)) System.out.println(acc); // [John, john00@mail.com, john_newyork@mail.com, johnsmith@mail.com] // [Mary, mary@mail.com] // [John, johnnybravo@mail.com] } }
Complexity
ApproachTimeSpaceNotes
Pairwise comparison + repeated mergingO(AΒ² Β· k)O(N)A accounts, k emails each
Email-graph DFSO(N log N)O(N)Build edges between emails, DFS, sort
Union-Find on accountsO(N Β· Ξ±(A) + N log N)O(N)Sorting the emails dominates
⚠ Common bugs & edge cases:
  • Merging by name β€” two different Johns would be combined. Only shared emails link accounts.
  • Forgetting the transitive case: A shares with B and B shares with C β‡’ A, B, C are one person even if A and C share nothing. Union-Find handles this automatically.
  • Not sorting the emails β€” the problem requires sorted order. A TreeSet sorts and de-duplicates in one step.
  • The original drill's complexity claim O(nΒ·Ξ±(n)) ignored the sort; it also crashed (IndexOutOfBounds on account.get(1)) for an account with a name but no emails.
10
Drill 78 Β· BFS Β· Implicit Graph

Word Ladder

🏷️

Difficulty

Hard

πŸ”—

LeetCode

LC 127 Β· Word Ladder

🧩

Pattern

BFS over words that differ by one letter

graph bfs implicit graph shortest path

Problem

Given beginWord, endWord and a dictionary wordList, return the number of words in the shortest transformation sequence from beginWord to endWord, where each step changes exactly one letter and every intermediate word must be in wordList. Return 0 if no sequence exists. beginWord itself need not be in the list.

Example
Input: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"] Output: 5 // hit β†’ hot β†’ dot β†’ dog β†’ cog Input: same, but wordList without "cog" Output: 0
Key insight: words are nodes; two words are adjacent if they differ in exactly one position. Every step costs 1, so the shortest sequence is a BFS shortest path. The graph is never built: from a word of length L, generate the 25Β·L candidate neighbours by substituting each letter, and keep those still in the unvisited dictionary set. Removing a word from the set the moment it is enqueued doubles as the visited mark.
From brute force to optimal

Building the explicit graph by comparing every pair of words costs O(NΒ²Β·L) before the search even starts. Generating neighbours by letter substitution costs O(26Β·L) candidates per word, each needing O(L) to build and hash, so the BFS is O(NΒ·26Β·LΒ²) β€” far better when N is large. DFS is wrong here: it finds a ladder, not the shortest. Bidirectional BFS (expanding from both ends, always the smaller frontier) is the standard follow-up optimisation.

Algorithm
  • Put the dictionary in a HashSet; if endWord is missing, return 0.
  • BFS from beginWord with level = 1.
  • For each polled word: if it equals endWord, return level. Otherwise try every position Γ— every letter; if the candidate is removed from the set (i.e. it was unvisited), enqueue it.
  • After each level, level++. Queue exhausted β‡’ return 0.
Walkthrough
BFS levels from hit to cog
hit hot dot lot dog log cog level 1 level 2 level 3 level 4 level 5 green = BFS tree edge (first discovery) Β· dashed = also one letter apart, but target already visited
LevelQueue at start of levelDiscovered (removed from set)
1hithot (hit β†’ hot)
2hotdot, lot (first letter changed)
3dot, lotdog (from dot), log (from lot)
4dog, logcog (from dog); log β†’ cog finds it already removed
5cogpolled word equals endWord β†’ return 5
Java β€” WordLadder.java (complete, runnable)
import java.util.ArrayDeque; import java.util.HashSet; import java.util.List; import java.util.Queue; import java.util.Set; public class WordLadder { public static int ladderLength(String beginWord, String endWord, List<String> wordList) { Set<String> unvisited = new HashSet<>(wordList); if (!unvisited.contains(endWord)) return 0; Queue<String> queue = new ArrayDeque<>(); queue.offer(beginWord); unvisited.remove(beginWord); int level = 1; // number of words in the sequence so far while (!queue.isEmpty()) { int size = queue.size(); for (int i = 0; i < size; i++) { String word = queue.poll(); if (word.equals(endWord)) return level; char[] chars = word.toCharArray(); for (int pos = 0; pos < chars.length; pos++) { char saved = chars[pos]; for (char ch = 'a'; ch <= 'z'; ch++) { if (ch == saved) continue; chars[pos] = ch; String next = new String(chars); if (unvisited.remove(next)) queue.offer(next); // remove = mark visited } chars[pos] = saved; } } level++; } return 0; } public static void main(String[] args) { List<String> words = List.of("hot", "dot", "dog", "lot", "log", "cog"); System.out.println(ladderLength("hit", "cog", words)); // 5 System.out.println(ladderLength("hit", "cog", List.of("hot", "dot", "dog", "lot", "log"))); // 0 } }
Complexity
ApproachTimeSpaceNotes
Explicit graph (pairwise compare) + BFSO(NΒ² Β· L)O(NΒ²)Edge construction dominates
BFS with letter substitutionO(N Β· 26 Β· LΒ²)O(N Β· L)LΒ² = L positions Γ— O(L) to build/hash each candidate
Bidirectional BFSsame bound, far smaller in practiceO(N Β· L)Meets in the middle
⚠ Common bugs & edge cases:
  • Returning the number of transformations (4) instead of the number of words (5).
  • Not checking that endWord is in the dictionary β€” the search explores everything and then returns 0 anyway, but the early exit is expected.
  • Marking visited on poll instead of on enqueue β€” the same word is enqueued from many parents and the queue explodes.
  • The original drill's complexity O(NΒ·26Β·L) ignored the O(L) cost of building and hashing each candidate string; the correct bound is O(NΒ·26Β·LΒ²).
11
Drill 79 Β· Greedy + Union-Find

Minimum Spanning Tree (Kruskal)

🏷️

Difficulty

Medium

πŸ”—

LeetCode

No exact LeetCode match β€” closest: LC 1584 Β· Min Cost to Connect All Points

🧩

Pattern

Sort edges + Union-Find

graph greedy union-find mst

Problem

Given a connected, undirected graph with n nodes and weighted edges [u, v, w], find the total weight of a minimum spanning tree β€” a subset of n βˆ’ 1 edges that connects all nodes with the smallest possible total weight. Return -1 if the graph is not connected.

Example
Input: n = 5, edges = [[0,1,1],[1,2,2],[0,2,3],[2,3,4],[1,3,5],[3,4,6],[2,4,7]] Output: 13 // edges 0-1 (1), 1-2 (2), 2-3 (4), 3-4 (6)
Key insight: the cut property: for any split of the nodes into two groups, the cheapest edge crossing the split belongs to some MST. Kruskal applies it greedily β€” consider edges from cheapest to most expensive and keep an edge exactly when it joins two different components. Union-Find answers "different components?" in near-O(1) and merges them on acceptance. An edge whose endpoints are already connected would close a cycle, and every edge on that cycle is cheaper, so it can be discarded safely.
From brute force to optimal

Enumerating every spanning tree is exponential (a complete graph has nⁿ⁻² of them). Kruskal sorts once β€” O(E log E) β€” and then does E near-constant Union-Find operations. Prim's algorithm is the alternative: grow one tree from a start node using a min-heap of crossing edges, O(E log V); it is preferred for dense graphs given as adjacency lists, Kruskal for sparse edge lists.

Algorithm
  • Copy and sort the edges by weight (don't mutate the caller's array).
  • For each edge, if union(u, v) succeeds, add its weight and count it.
  • Stop once n βˆ’ 1 edges are taken.
  • Return the total if n βˆ’ 1 edges were taken, else -1.
Walkthrough
Kruskal on the example β€” greedy by weight, skip cycle-closing edges
1 2 3 4 5 6 7 0 1 2 3 4 green = in MST Β· red = rejected (would close a cycle) Β· grey = never examined SORTED EDGES 0-1 w=1 take 1-2 w=2 take 0-2 w=3 cycle 2-3 w=4 take 1-3 w=5 cycle 3-4 w=6 take 2-4 w=7 stop total = 1+2+4+6 = 13
Edge (w)find(u), find(v)DecisionTotalEdges used
0-1 (1)0, 1take; parent[1]=011
1-2 (2)0, 2take; parent[2]=032
0-2 (3)0, 0same root β†’ reject (cycle 0-1-2)32
2-3 (4)0, 3take; parent[3]=073
1-3 (5)0, 0reject (cycle 1-2-3)73
3-4 (6)0, 4take; parent[4]=0134 = nβˆ’1 β†’ stop
Java β€” KruskalMST.java (complete, runnable)
import java.util.Arrays; public class KruskalMST { private static int[] parent, rank; private static int find(int x) { if (parent[x] != x) parent[x] = find(parent[x]); return parent[x]; } private static boolean union(int a, int b) { int ra = find(a), rb = find(b); if (ra == rb) return false; if (rank[ra] < rank[rb]) { int t = ra; ra = rb; rb = t; } parent[rb] = ra; if (rank[ra] == rank[rb]) rank[ra]++; return true; } // edges[i] = {u, v, weight}. Returns MST weight, or -1 if the graph is disconnected. public static int minSpanningTree(int n, int[][] edges) { parent = new int[n]; rank = new int[n]; for (int i = 0; i < n; i++) parent[i] = i; int[][] sorted = edges.clone(); // don't reorder the caller's array Arrays.sort(sorted, (a, b) -> Integer.compare(a[2], b[2])); int total = 0, used = 0; for (int[] e : sorted) { if (union(e[0], e[1])) { // joins two components -> keep total += e[2]; if (++used == n - 1) break; // tree complete } } return used == n - 1 ? total : -1; } public static void main(String[] args) { int[][] edges = {{0, 1, 1}, {1, 2, 2}, {0, 2, 3}, {2, 3, 4}, {1, 3, 5}, {3, 4, 6}, {2, 4, 7}}; System.out.println(minSpanningTree(5, edges)); // 13 (edges 0-1, 1-2, 2-3, 3-4) System.out.println(minSpanningTree(3, new int[][]{{0, 1, 5}})); // -1 (node 2 unreachable) } }
Complexity
ApproachTimeSpaceNotes
Enumerate spanning treesexponentialβ€”Infeasible
Prim with a binary heapO(E log V)O(V + E)Better for dense adjacency lists
KruskalO(E log E)O(V + E)Sort dominates; Union-Find is ~O(1) per edge
⚠ Common bugs & edge cases:
  • Sorting the caller's edges array in place (the original drill did) β€” a hidden side effect; sort a copy.
  • Comparator (a, b) -> a[2] - b[2] can overflow for large weights; use Integer.compare.
  • Accumulating the total in an int when weights can be up to 10⁹ β€” use long in production.
  • Forgetting the disconnected case: fewer than n βˆ’ 1 accepted edges means there is no spanning tree.
  • Confusing MST with shortest paths: the MST minimises the sum of all chosen edges, not the distance between any particular pair.
12
Drill 80 Β· Weighted Shortest Path

Shortest Path (Dijkstra)

🏷️

Difficulty

Medium

πŸ”—

LeetCode

No exact LeetCode match β€” closest: LC 743 Β· Network Delay Time

🧩

Pattern

Min-heap + relaxation (Template 4)

graph dijkstra priority queue greedy

Problem

Given an undirected graph with n nodes and edges [u, v, w] with non-negative weights, and a source node src, return the shortest distance from src to every node (Integer.MAX_VALUE for unreachable nodes).

Example
Input: n = 5, edges = [[0,1,4],[0,2,1],[2,1,2],[1,3,1],[2,3,5],[3,4,3]], src = 0 Output: [0, 3, 1, 4, 7] // 0β†’2β†’1 (1+2=3) beats the direct edge 0β†’1 (4); 0β†’2β†’1β†’3 (4) beats 0β†’2β†’3 (6)
Key insight: with non-negative weights, the unsettled node with the smallest tentative distance is already final β€” any other route to it would pass through a node that is at least as far away and then add a non-negative edge. So repeatedly pop the minimum from a heap, and relax its edges: if dist[u] + w < dist[v], record the improvement and push (v, newDist). Java's PriorityQueue has no decrease-key, so old entries stay in the heap; skip them on poll with if (d > dist[u]) continue.
From brute force to optimal

BFS is wrong as soon as weights differ (it minimises edge count). Bellman-Ford relaxes every edge Vβˆ’1 times β€” O(VΒ·E) β€” and handles negative weights. Dijkstra with an array scan for the minimum is O(VΒ²), good for dense graphs; with a binary heap it is O((V + E) log V), the standard interview answer for sparse graphs.

Algorithm
  • Build the adjacency list (both directions for undirected edges).
  • dist[] = MAX, dist[src] = 0, push (src, 0).
  • Poll the smallest; skip it if stale; otherwise relax every edge and push improvements.
  • Return dist when the heap is empty.
Walkthrough
Final distances and the shortest-path tree
4 1 2 1 5 3 0 1 2 3 4 d=0 d=3 d=1 d=4 d=7 green edges = shortest-path tree from 0 0β†’1 direct = 4 0β†’2β†’1 = 3 wins
Poll (node, d)RelaxationsHeap afterdist [0..4]
(0, 0)1: βˆžβ†’4, 2: βˆžβ†’1(2,1) (1,4)[0, 4, 1, ∞, ∞]
(2, 1)1: 4β†’3, 3: βˆžβ†’6(1,3) (1,4) (3,6)[0, 3, 1, 6, ∞]
(1, 3)3: 6β†’4(1,4) (3,4) (3,6)[0, 3, 1, 4, ∞]
(1, 4)stale (4 > dist[1]=3) β†’ skip(3,4) (3,6)unchanged
(3, 4)4: βˆžβ†’7(3,6) (4,7)[0, 3, 1, 4, 7]
(3, 6)stale β†’ skip(4,7)unchanged
(4, 7)no improvementsemptyfinal
Java β€” Dijkstra.java (complete, runnable)
import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.PriorityQueue; public class Dijkstra { // edges[i] = {u, v, w}, undirected, w >= 0. Returns dist[], Integer.MAX_VALUE = unreachable. public static int[] shortestPaths(int n, int[][] edges, int src) { List<List<int[]>> graph = new ArrayList<>(); for (int i = 0; i < n; i++) graph.add(new ArrayList<>()); for (int[] e : edges) { graph.get(e[0]).add(new int[]{e[1], e[2]}); graph.get(e[1]).add(new int[]{e[0], e[2]}); } int[] dist = new int[n]; Arrays.fill(dist, Integer.MAX_VALUE); dist[src] = 0; // entries are {node, distance}; order by distance, then node id PriorityQueue<int[]> pq = new PriorityQueue<>( (a, b) -> a[1] != b[1] ? Integer.compare(a[1], b[1]) : Integer.compare(a[0], b[0])); pq.offer(new int[]{src, 0}); while (!pq.isEmpty()) { int[] top = pq.poll(); int u = top[0], d = top[1]; if (d > dist[u]) continue; // stale entry: u already settled cheaper for (int[] nb : graph.get(u)) { int v = nb[0], w = nb[1]; if (d + w < dist[v]) { // relax dist[v] = d + w; pq.offer(new int[]{v, dist[v]}); } } } return dist; } public static void main(String[] args) { int[][] edges = {{0, 1, 4}, {0, 2, 1}, {2, 1, 2}, {1, 3, 1}, {2, 3, 5}, {3, 4, 3}}; System.out.println(Arrays.toString(shortestPaths(5, edges, 0))); // [0, 3, 1, 4, 7] } }
Tie-breaking: the comparator orders by distance, then by node id, so the trace above is deterministic β€” (1,4) is polled before (3,4). With a distance-only comparator the two could come out in either order; the final distances are the same either way.
Complexity
ApproachTimeSpaceNotes
BFS (ignores weights)O(V + E)O(V)Wrong answer for weighted graphs
Bellman-FordO(V Β· E)O(V)Handles negative weights
Dijkstra, array scanO(VΒ²)O(V)Best for very dense graphs
Dijkstra, binary heapO((V + E) log V)O(V + E)Lazy deletion of stale entries
⚠ Common bugs & edge cases:
  • Comparator (a, b) -> a[1] - b[1] (used in the original drill) overflows when distances approach Integer.MAX_VALUE; use Integer.compare.
  • Missing the stale-entry check β€” still correct, but nodes are re-processed and the bound degrades.
  • Adding w to Integer.MAX_VALUE for an unreachable node overflows to a negative number; relaxing only from polled (reachable) nodes avoids it.
  • Using Dijkstra with negative edge weights β€” a settled node can later be improved, and the greedy guarantee breaks.
  • Adding only one direction of each undirected edge.
13
Chapter Thirteen Β· Summary

Graphs at a Glance

Nine drills, four templates. The table is the one-page revision sheet: if you can say why each row uses its pattern β€” and what would go wrong with the neighbouring pattern β€” you are ready for graph questions in interviews.

DrillProblemPatternTimeSpaceDifficulty
72Clone GraphDFS + original→copy mapO(V + E)O(V)Medium
73Number of Connected ComponentsUnion-Find counterO(V + EΒ·Ξ±(V))O(V)Medium
74Graph Valid Treenβˆ’1 edges + Union-FindO(V + EΒ·Ξ±(V))O(V)Medium
75Course Schedule3-colour DFS cycle checkO(V + E)O(V + E)Medium
76Topological SortKahn's BFS on in-degreesO(V + E)O(V + E)Medium
77Accounts MergeUnion-Find + TreeSetO(N log N)O(N)Medium
78Word LadderBFS on implicit graphO(NΒ·26Β·LΒ²)O(NΒ·L)Hard
79Minimum Spanning Tree (Kruskal)Sort + Union-FindO(E log E)O(V + E)Medium
80Shortest Path (Dijkstra)Min-heap + relaxationO((V + E) log V)O(V + E)Medium
Self-test before moving on:
  • Write Union-Find with path compression and union by rank from memory, and explain what each optimisation prevents.
  • Explain why one visited flag detects cycles in an undirected graph (with parent tracking) but not in a directed one.
  • Re-solve Course Schedule with Kahn's algorithm and compare it with the 3-colour DFS.
  • Give a 3-node example where BFS returns the wrong weighted shortest path, and one where Dijkstra fails because of a negative edge.
  • Implement Prim's algorithm for Drill 79 and confirm it also returns 13.
What to practice next
➑️

Next page

09 Β· Dynamic Programming β€” shortest paths on DAGs are DP in disguise; the next page makes that explicit.

← 07 Β· Grids & Matrices All Coding Drills 09 Β· Dynamic Programming β†’