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.
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.
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.
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.
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.
visited set (or map) is mandatory β it makes every traversal O(V + E) and prevents infinite loopsfind(a) == find(b); adding an edge inside one group means that edge closes a cycleFour 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 | Answers | Time | Space | Drills |
|---|---|---|---|---|
| DFS / BFS + visited | reachability, copy, unweighted distance | O(V + E) | O(V) | 72, 78 |
| Union-Find | connectivity, cycle in undirected graph, grouping | O(E Β· Ξ±(V)) | O(V) | 73, 74, 77, 79 |
| Kahn / 3-colour DFS | dependency order, cycle in directed graph | O(V + E) | O(V + E) | 75, 76 |
| Dijkstra | weighted shortest paths, w β₯ 0 | O((V + E) log V) | O(V + E) | 80 |
Clone Graph
graph dfs hashmap deep copy
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.
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.
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).
- If the input is
null, returnnull. 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.
| Step | Call | Map after | Result |
|---|---|---|---|
| 1 | clone(1) | {1} | create 1', recurse into neighbour 2 |
| 2 | clone(2) | {1, 2} | neighbour 1 is in map β link 2'β1'; recurse into 3 |
| 3 | clone(3) | {1, 2, 3} | neighbour 2 in map β link; recurse into 4 |
| 4 | clone(4) | {1, 2, 3, 4} | neighbours 1 and 3 both in map β link both, return 4' |
| 5 | back in clone(1) | unchanged | neighbour 4 in map β link 1'β4'; return 1' |
| Approach | Time | Space | Notes |
|---|---|---|---|
| Naive recursion (no map) | β on cycles | β | Never terminates |
| DFS / BFS + HashMap | O(V + E) | O(V) | Map holds V copies; recursion depth up to V |
- 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
valis fine only when values are unique; keying by the node object is always safe. - Forgetting
nullinput β 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.
Number of Connected Components
Difficulty
Medium
Pattern
Union-Find with a component counter
graph union-find connected components
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.
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.
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).
- Initialise
parent[i] = i,rank[i] = 0,components = n. - For each edge
(a, b), callunion(a, b); on success,components--. - Return
components.
| Edge | find(a), find(b) | Action | parent[] | components |
|---|---|---|---|---|
| start | β | β | [0, 1, 2, 3, 4] | 5 |
| (0,1) | 0, 1 | equal ranks β parent[1]=0, rank[0]=1 | [0, 0, 2, 3, 4] | 4 |
| (1,2) | 0, 2 | rank 1 > 0 β parent[2]=0 | [0, 0, 0, 3, 4] | 3 |
| (3,4) | 3, 4 | equal ranks β parent[4]=3, rank[3]=1 | [0, 0, 0, 3, 3] | 2 |
| Approach | Time | Space | Notes |
|---|---|---|---|
| DFS from each unvisited node | O(V + E) | O(V + E) | Needs the adjacency list |
| Union-Find, no optimisations | O(V Β· E) | O(V) | Degenerate chains |
| Union-Find, rank + compression | O(V + E Β· Ξ±(V)) | O(V) | Ξ± β€ 4 for any realistic V |
- 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] = binstead ofparent[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)).
Graph Valid Tree
Difficulty
Medium
Pattern
Edge count + Union-Find cycle check
graph union-find tree property
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.
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.
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.
- If
edges.length != n - 1, returnfalse. - For each edge, if
union(a, b)fails (same root), returnfalse. - Return
true.
| Input | Edge | Roots | Verdict |
|---|---|---|---|
| valid | (0,1) (0,2) (0,3) (1,4) | always different | 4 successful unions β true |
| cyclic | (0,1) | 0, 1 | union β parent[1]=0 |
| cyclic | (1,2) | 0, 2 | union β parent[2]=0 |
| cyclic | (2,0) | 0, 0 | same root β return false |
| Approach | Time | Space | Notes |
|---|---|---|---|
| DFS with parent tracking | O(V + E) | O(V + E) | Two checks: no back edge, all visited |
| Edge count + Union-Find | O(V + E Β· Ξ±(V)) | O(V) | Early exit on wrong edge count |
- Checking only "no cycle" β a forest (two separate trees) has no cycle but is not a tree. The
n β 1edge 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).
Course Schedule
graph dfs cycle detection directed
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.
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).
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.
- Build the adjacency list with edges
b β afor 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
falseif any cycle is found, otherwisetrue.
| Call | Colours (0 1 2 3) | Event |
|---|---|---|
| hasCycle(0) | G W W W | neighbour 1 is WHITE β recurse |
| hasCycle(1) | G G W W | neighbour 2 is WHITE β recurse |
| hasCycle(2) | G G G W | neighbour 3 is WHITE β recurse |
| hasCycle(3) | G G G G | neighbour 1 is GRAY β cycle β true propagates up β canFinish returns false |
[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.
| Approach | Time | Space | Notes |
|---|---|---|---|
| Try all orderings | O(V!) | O(V) | Infeasible |
| DFS with on-path flag only | exponential worst case | O(V) | Re-explores shared sub-DAGs |
| 3-colour DFS | O(V + E) | O(V + E) | Recursion depth up to V |
- Using a single
visitedflag 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.
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
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.
n, there is a cycle.
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.
- 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
nnodes return it; otherwise return[].
| Poll | Decrement | Newly zero β enqueue | Queue after | in-degree [0..5] |
|---|---|---|---|---|
| start | β | 4, 5 | [4, 5] | [2, 2, 1, 1, 0, 0] |
| 4 | 0: 2β1, 1: 2β1 | β | [5] | [1, 1, 1, 1, 0, 0] |
| 5 | 2: 1β0, 0: 1β0 | 2, 0 | [2, 0] | [0, 1, 0, 1, 0, 0] |
| 2 | 3: 1β0 | 3 | [0, 3] | [0, 1, 0, 0, 0, 0] |
| 0 | (no out-edges) | β | [3] | unchanged |
| 3 | 1: 1β0 | 1 | [1] | [0, 0, 0, 0, 0, 0] |
| 1 | (no out-edges) | β | [] | 6 nodes output β valid |
| Approach | Time | Space | Notes |
|---|---|---|---|
| Try all permutations | O(n! Β· E) | O(n) | Infeasible |
| DFS post-order + reverse | O(V + E) | O(V + E) | Needs a separate cycle check |
| Kahn's algorithm | O(V + E) | O(V + E) | Order + cycle detection in one pass |
- The original drill used DFS with
java.util.Stackand 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.
Accounts Merge
graph union-find hashmap sorting
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].
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.
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.
parent[i] = ifor each account.- For each email of account
i:owner.putIfAbsent(email, i); if it already had an ownerp, unioniwithp. - For each
(email, owner), add the email to aTreeSetkeyed byfind(owner). - Emit
[name of root account] + sorted emailsfor each root.
| Account | Owner map | Union | |
|---|---|---|---|
| A0 | johnsmith, john_newyork | both β A0 | β |
| A1 | johnsmith | already A0 | union(A1, A0): parent[A1] = A0 |
| A1 | john00 | β A1 | β |
| A2 | mary | β A2 | β |
| A3 | johnnybravo | β A3 | β (same name as A0, but no shared email) |
| Approach | Time | Space | Notes |
|---|---|---|---|
| Pairwise comparison + repeated merging | O(AΒ² Β· k) | O(N) | A accounts, k emails each |
| Email-graph DFS | O(N log N) | O(N) | Build edges between emails, DFS, sort |
| Union-Find on accounts | O(N Β· Ξ±(A) + N log N) | O(N) | Sorting the emails dominates |
- 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
TreeSetsorts and de-duplicates in one step. - The original drill's complexity claim O(nΒ·Ξ±(n)) ignored the sort; it also crashed (
IndexOutOfBoundsonaccount.get(1)) for an account with a name but no emails.
Word Ladder
graph bfs implicit graph shortest path
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.
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.
- Put the dictionary in a
HashSet; ifendWordis missing, return 0. - BFS from
beginWordwithlevel = 1. - For each polled word: if it equals
endWord, returnlevel. 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.
| Level | Queue at start of level | Discovered (removed from set) |
|---|---|---|
| 1 | hit | hot (hit β hot) |
| 2 | hot | dot, lot (first letter changed) |
| 3 | dot, lot | dog (from dot), log (from lot) |
| 4 | dog, log | cog (from dog); log β cog finds it already removed |
| 5 | cog | polled word equals endWord β return 5 |
| Approach | Time | Space | Notes |
|---|---|---|---|
| Explicit graph (pairwise compare) + BFS | O(NΒ² Β· L) | O(NΒ²) | Edge construction dominates |
| BFS with letter substitution | O(N Β· 26 Β· LΒ²) | O(N Β· L) | LΒ² = L positions Γ O(L) to build/hash each candidate |
| Bidirectional BFS | same bound, far smaller in practice | O(N Β· L) | Meets in the middle |
- Returning the number of transformations (4) instead of the number of words (5).
- Not checking that
endWordis 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Β²).
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
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.
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.
- 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 β 1edges are taken. - Return the total if
n β 1edges were taken, else-1.
| Edge (w) | find(u), find(v) | Decision | Total | Edges used |
|---|---|---|---|---|
| 0-1 (1) | 0, 1 | take; parent[1]=0 | 1 | 1 |
| 1-2 (2) | 0, 2 | take; parent[2]=0 | 3 | 2 |
| 0-2 (3) | 0, 0 | same root β reject (cycle 0-1-2) | 3 | 2 |
| 2-3 (4) | 0, 3 | take; parent[3]=0 | 7 | 3 |
| 1-3 (5) | 0, 0 | reject (cycle 1-2-3) | 7 | 3 |
| 3-4 (6) | 0, 4 | take; parent[4]=0 | 13 | 4 = nβ1 β stop |
| Approach | Time | Space | Notes |
|---|---|---|---|
| Enumerate spanning trees | exponential | β | Infeasible |
| Prim with a binary heap | O(E log V) | O(V + E) | Better for dense adjacency lists |
| Kruskal | O(E log E) | O(V + E) | Sort dominates; Union-Find is ~O(1) per edge |
- Sorting the caller's
edgesarray 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; useInteger.compare. - Accumulating the total in an
intwhen weights can be up to 10βΉ β uselongin production. - Forgetting the disconnected case: fewer than
n β 1accepted 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.
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
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).
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.
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.
- 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
distwhen the heap is empty.
| Poll (node, d) | Relaxations | Heap after | dist [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 improvements | empty | final |
| Approach | Time | Space | Notes |
|---|---|---|---|
| BFS (ignores weights) | O(V + E) | O(V) | Wrong answer for weighted graphs |
| Bellman-Ford | O(V Β· E) | O(V) | Handles negative weights |
| Dijkstra, array scan | O(VΒ²) | O(V) | Best for very dense graphs |
| Dijkstra, binary heap | O((V + E) log V) | O(V + E) | Lazy deletion of stale entries |
- Comparator
(a, b) -> a[1] - b[1](used in the original drill) overflows when distances approachInteger.MAX_VALUE; useInteger.compare. - Missing the stale-entry check β still correct, but nodes are re-processed and the bound degrades.
- Adding
wtoInteger.MAX_VALUEfor 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.
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.
| Drill | Problem | Pattern | Time | Space | Difficulty |
|---|---|---|---|---|---|
| 72 | Clone Graph | DFS + originalβcopy map | O(V + E) | O(V) | Medium |
| 73 | Number of Connected Components | Union-Find counter | O(V + EΒ·Ξ±(V)) | O(V) | Medium |
| 74 | Graph Valid Tree | nβ1 edges + Union-Find | O(V + EΒ·Ξ±(V)) | O(V) | Medium |
| 75 | Course Schedule | 3-colour DFS cycle check | O(V + E) | O(V + E) | Medium |
| 76 | Topological Sort | Kahn's BFS on in-degrees | O(V + E) | O(V + E) | Medium |
| 77 | Accounts Merge | Union-Find + TreeSet | O(N log N) | O(N) | Medium |
| 78 | Word Ladder | BFS on implicit graph | O(NΒ·26Β·LΒ²) | O(NΒ·L) | Hard |
| 79 | Minimum Spanning Tree (Kruskal) | Sort + Union-Find | O(E log E) | O(V + E) | Medium |
| 80 | Shortest Path (Dijkstra) | Min-heap + relaxation | O((V + E) log V) | O(V + E) | Medium |
- Write Union-Find with path compression and union by rank from memory, and explain what each optimisation prevents.
- Explain why one
visitedflag 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.
Next page
09 Β· Dynamic Programming β shortest paths on DAGs are DP in disguise; the next page makes that explicit.
Theory refresh
More problems
β 07 Β· Grids & Matrices All Coding Drills 09 Β· Dynamic Programming β