Chapter 14: Graphs
Introduction
Learning Outcomes
Explain vertices, edges, direction, weights, paths, and connectivity.
Compare adjacency matrices and adjacency lists by their costs.
Run BFS, DFS, and topological sorting on DAGs, and interpret the results.
Match unweighted BFS, Dijkstra, Kruskal, and Prim to the problems they solve.
Basic Terms
Graphs
- A graph is a set of vertices (or nodes) together with edges that connect pairs of them.
- Graphs model complex relationships, networks, and structures.
- Graph theory studies the relationships between vertices and edges.
Example: V = {0, 1, 2, 3, 4}, E = {(0,1), (0,4), (1,2), (1,3), (1,4), (2,3), (3,4)} · n = |V| = 5, m = |E| = 7
Basic Terms
Vertices (Nodes)
- The basic building block of a graph.
- Represents an object such as a city, a person, or a computer.
- A vertex can store a value (a label, a name, a location…).
- Example: cities and intersections on a map.
Road map: cities are vertices, roads are edges
Basic Terms
Edges
- Edges are the connections between vertices.
- An edge represents a relationship between two vertices.
- If (v, w) is an edge, v and w are adjacent: each is a neighbor of the other, and the edge is incident to both.
- Examples: roads between cities, links in a computer network, friendships in a social network.
A social network: people are vertices, relationships are edges
Basic Terms
Degree
- The degree of a vertex is the number of edges incident to it:
deg(v). - A vertex of degree 0 is called isolated.
- A self-loop adds 2 to the degree.
- Example: the number of roads meeting at an intersection.
Each vertex shows its own degree
Basic Terms
Directed Graphs (Digraphs)
- Each edge points in one direction, from one vertex to another: (u, v) ≠ (v, u).
- There may be no way back: here, A cannot be reached from B.
- A directed graph is also called a digraph.
- Examples: web pages and their links, course prerequisites, one-way streets.
| Vertex | A | B | C | D | E | F |
|---|---|---|---|---|---|---|
| in-degree | 0 | 2 | 1 | 1 | 1 | 1 |
| out-degree | 1 | 1 | 1 | 1 | 2 | 0 |
Basic Terms
Paths and Cycles
- Path: a sequence of vertices in which each consecutive pair is joined by an edge.
- Simple path: a path that repeats no vertex.
- Cycle: a path that starts and ends at the same vertex.
- Simple cycle: repeats no vertex except the start.
- Graphs may have cycles; trees never do.
Green: A-B-C-D-E-F-A · Red (dashed): G-H-I-J-K-L-G
Basic Terms
An Example Graph
- V = {1, 2, 3, 4, 5}
- E = {(1,2), (1,3), (1,4), (2,5), (3,4), (4,5)}
- Vertices 1 and 2 are adjacent.
- deg(1) = 3, deg(4) = 3, all others 2 → total 12 = 2·6 ✓
| Sequence | Kind |
|---|---|
| 1 → 2 → 5 | simple path |
| 1 → 3 → 4 → 1 | simple cycle |
| 1 → 3 → 4 → 1 → 2 | non-simple path (1 repeats) |
| 1 → 3 → 4 → 1 → 4 → 1 | non-simple cycle |
Basic Terms
Connected and Complete Graphs
- Connected graph: there is a path between every pair of distinct vertices.
- Connected component: a maximal group of vertices that can reach one another.
- Complete graph: there is an edge between every pair of distinct vertices.
A simple undirected graph on n vertices has m ≤ n(n−1)/2, with equality for the complete graph. A connected graph has m ≥ n − 1.
Basic Terms
An Example Digraph
- V = {1, 2, 3, 4, 5}
- E = {(1,2), (1,4), (2,5), (4,5), (3,1), (4,3)}
- Vertex 2 is adjacent to vertex 1 (there is an edge 1 → 2).
- Vertex 1 is not adjacent to vertex 2!
| Sequence | Kind |
|---|---|
| 1 → 2 → 5 | directed path |
| 1 → 4 → 3 → 1 | directed cycle |
| 5 → … | no edges leave 5 |
A directed edge (u, v) is an ordered pair: u is its tail (origin), v its head (destination).
Basic Terms
Weighted Graphs
- Each edge carries a number, its weight: a distance, time, cost, capacity…
- The length of a path is the sum of its edge weights. Weighted graphs can be undirected or directed.
Basic Terms
The Graph ADT
The core operations of Goodrich's Graph interface (V: vertex, E: edge):
| Operation | Description |
|---|---|
numVertices(), numEdges() | Number of vertices n, number of edges m |
vertices(), edges() | Iterate over all vertices / all edges |
getEdge(u, v) | The edge from u to v, or null if there is none |
endVertices(e), opposite(v, e) | Both ends of an edge / the end of e other than v |
outDegree(v), inDegree(v) | Out-degree / in-degree (equal in an undirected graph) |
outgoingEdges(v), incomingEdges(v) | Edges leaving v / entering v |
insertVertex(x), insertEdge(u, v, x) | Add a new vertex / a new edge |
removeVertex(v), removeEdge(e) | Remove a vertex (with its incident edges) / an edge |
What these operations cost depends on how the graph is stored in memory.
Graph Representations
Storing a Graph: Three Representations
The edge list is the simplest representation:
- Space O(n + m); adding an edge is O(1).
- But "Are u and v adjacent?" or "Who are u's neighbors?" means scanning the whole list: O(m).
Graph Representations
Adjacency Matrix
- An n×n array:
matrix[i][j]= 1 if there is an edge from vertex i to j, and 0 otherwise. - In a weighted graph,
matrix[i][j]holds the edge weight, or∞(or some special value) if there is no edge. - In an undirected graph, the matrix is symmetric:
matrix[i][j] == matrix[j][i]. - Space is O(n²), so memory grows quickly with the number of vertices.
Easy
- Is there an edge between two vertices? O(1)
- Adding / removing an edge: O(1)
Hard
- Finding a vertex's neighbors: a whole row, O(n)
- In a sparse graph, most cells are empty (0)
Graph Representations
Adjacency Matrix: Three Examples
| A | B | C | D | E | |
|---|---|---|---|---|---|
| A | 0 | 1 | 0 | 0 | 1 |
| B | 1 | 0 | 1 | 0 | 1 |
| C | 0 | 1 | 0 | 1 | 0 |
| D | 0 | 0 | 1 | 0 | 1 |
| E | 1 | 1 | 0 | 1 | 0 |
Undirected graph: each edge fills two cells, so the matrix is symmetric about the diagonal. Row sum = degree (B: 3).
| A | B | C | D | E | |
|---|---|---|---|---|---|
| A | 0 | 1 | 0 | 0 | 1 |
| B | 0 | 0 | 1 | 1 | 0 |
| C | 0 | 0 | 0 | 0 | 0 |
| D | 1 | 0 | 1 | 0 | 1 |
| E | 0 | 1 | 0 | 1 | 0 |
Directed graph: row = origin, column = destination. Not symmetric; a row sum is an out-degree, a column sum an in-degree.
| A | B | C | D | E | |
|---|---|---|---|---|---|
| A | 0 | 4 | ∞ | ∞ | ∞ |
| B | ∞ | 0 | 3 | 3 | ∞ |
| C | 2 | ∞ | 0 | ∞ | 2 |
| D | ∞ | ∞ | ∞ | 0 | ∞ |
| E | ∞ | 5 | ∞ | 2 | 0 |
Weighted graph: a cell holds the weight, or ∞ if there is no edge (0 on the diagonal). Writing 0 in an empty cell could be confused with an edge of weight 0.
Graph Representations
Adjacency List
- Keep n lists for n vertices (usually linked lists or dynamic arrays).
list[i]holds the vertices adjacent to vertex i (with weights, if the graph is weighted).- In an undirected graph, each edge (v, w) goes into both lists: w into v's list, v into w's.
- Space is O(n + m), ideal for sparse graphs.
Easy
- Visiting a vertex's neighbors: O(deg(v))
- Adding an edge: O(1)
Hard
- Is there an edge between u and v? Scan the list: O(deg(u))
Graph Representations
Adjacency List: Three Examples
Undirected: 6 edges → 12 list entries (each edge twice). List length = degree.
Directed: only outgoing edges are listed; 9 edges → 9 entries. No edge leaves C, so its list is empty.
Weighted: each entry is a (neighbor, weight) pair; e.g. B's list: (C, 3), (D, 3).
Graph Representations
Representations: Memory and Operation Costs
| Operation (n vertices, m edges) | Edge list | Adjacency list | Adjacency matrix |
|---|---|---|---|
| Memory | O(n + m) | O(n + m) | O(n²) |
Is there an edge u–v? getEdge | O(m) | undirected: O(min(deg u, deg v)) directed u→v: O(outDeg u) | O(1) |
| Visit the neighbors of v | O(m) | O(deg v) | O(n) |
Graph Representations
An Adjacency List in Java
class Graph { private final int n; // vertices: 0..n-1 private final List<List<Integer>> adj = new ArrayList<>(); Graph(int n) { this.n = n; for (int i = 0; i < n; i++) adj.add(new ArrayList<>()); } void addEdge(int u, int v) { adj.get(u).add(v); adj.get(v).add(u); // omit this line in a digraph } List<Integer> neighbors(int u) { return adj.get(u); } // O(1) int degree(int u) { return adj.get(u).size(); } }
boolean[][] m = new boolean[n][n]; add an edge: m[u][v] = m[v][u] = true; query m[u][v] in O(1).int[]{v, w} or a small Edge class; a matrix uses int[][] with Integer.MAX_VALUE for a missing edge.Graph Representations · Step through the code
Code Trace: Building an Adjacency List
Graph(int n) { this.n = n; for (int i = 0; i < n; i++) adj.add(new ArrayList<>()); } void addEdge(int u, int v) { adj.get(u).add(v); adj.get(v).add(u); // not in a digraph } // main: Graph g = new Graph(5); int[][] E = {{0,1}, {0,4}, {1,2}, {1,4}, {2,3}, {3,4}}; for (int[] e : E) g.addEdge(e[0], e[1]);
new Graph(5): the constructor runs and sets n = 5. adj is still an empty outer list.
i = 0: add an empty inner list for vertex 0.
i = 1: add an empty inner list for vertex 1.
i = 2: add an empty inner list for vertex 2.
i = 3: add an empty inner list for vertex 3.
i = 4: add an empty inner list for vertex 4. Every vertex now has an (empty) neighbor list.
Edge 0–1: 1 is appended to 0's list.
Undirected: the same edge is added in reverse → 0 goes into 1's list.
Edge 0–4: 4 is appended to 0's list.
Undirected: the same edge is added in reverse → 0 goes into 4's list.
Edge 1–2: 2 is appended to 1's list.
Undirected: the same edge is added in reverse → 1 goes into 2's list.
Edge 1–4: 4 is appended to 1's list.
Undirected: the same edge is added in reverse → 1 goes into 4's list.
Edge 2–3: 3 is appended to 2's list.
Undirected: the same edge is added in reverse → 2 goes into 3's list.
Edge 3–4: 4 is appended to 3's list.
Undirected: the same edge is added in reverse → 3 goes into 4's list.
Done: 6 edges → 2·6 = 12 list entries. Each addEdge is O(1); memory O(n + m). degree(1) = list length = 3.
Graph Representations · Step through the code
Code Trace: Building an Adjacency Matrix
A 5×5 boolean matrix is created; Java initializes every cell to false (0). Memory O(n²) = 25 cells.
Edge 0–1: m[0][1] = true (row 0, column 1).
Mirror cell: m[1][0] = true. An undirected graph's matrix is symmetric about the diagonal.
Edge 0–4: m[0][4] = true (row 0, column 4).
Mirror cell: m[4][0] = true. An undirected graph's matrix is symmetric about the diagonal.
Edge 1–2: m[1][2] = true (row 1, column 2).
Mirror cell: m[2][1] = true. An undirected graph's matrix is symmetric about the diagonal.
Edge 1–4: m[1][4] = true (row 1, column 4).
Mirror cell: m[4][1] = true. An undirected graph's matrix is symmetric about the diagonal.
Edge 2–3: m[2][3] = true (row 2, column 3).
Mirror cell: m[3][2] = true. An undirected graph's matrix is symmetric about the diagonal.
Edge 3–4: m[3][4] = true (row 3, column 4).
Mirror cell: m[4][3] = true. An undirected graph's matrix is symmetric about the diagonal.
Done: 12 cells are true, 13 are false and wasted. "Is there an edge 2–4?" → one access to m[2][4], O(1); but finding the neighbors of 2 means scanning a whole row: O(n).
boolean[][] m = new boolean[n][n]; void addEdge(int u, int v) { m[u][v] = true; m[v][u] = true; // not in a digraph } // main: same 6 edges (n = 5) for (int[] e : E) g.addEdge(e[0], e[1]);
Traversal (BFS / DFS)
Traversal Algorithms
A traversal systematically visits every vertex reachable from a start vertex, each exactly once.
boolean[] visited) is essential; without it the algorithm can loop forever.Traversal (BFS / DFS)
Breadth-First Search (BFS)
- Explores vertices level by level: first those 1 edge from the start, then those 2 edges away…
- Uses a queue: first discovered, first processed.
- Each vertex is visited exactly once.
Number = visit order; bold = BFS tree
Pros
- Finds shortest paths (fewest edges) in unweighted graphs.
- Discovers nearby vertices quickly.
Con
- Can use a lot of memory: a whole level waits in the queue.
Traversal (BFS / DFS)
The BFS Algorithm
BFS(start): queue.add(start) mark start visited while queue is not empty: w = queue.remove() for each unvisited neighbor u of w: mark u visited queue.add(u)
A vertex is marked when it enters the queue, so no vertex enters the queue twice.
void bfs(int s) { boolean[] visited = new boolean[n]; Queue<Integer> q = new ArrayDeque<>(); visited[s] = true; q.add(s); while (!q.isEmpty()) { int w = q.poll(); System.out.print(w + " "); for (int u : adj.get(w)) if (!visited[u]) { visited[u] = true; q.add(u); } } }
Traversal (BFS / DFS) · Step through the code
Code Trace: bfs(A)
void bfs(int s) { boolean[] visited = new boolean[n]; Queue<Integer> q = new ArrayDeque<>(); visited[s] = true; q.add(s); while (!q.isEmpty()) { int w = q.poll(); System.out.print(w + " "); for (int u : adj.get(w)) if (!visited[u]) { visited[u] = true; q.add(u); } } }
s = A (A=0, B=1, …, I=8; neighbors in alphabetical order). visited is all false; the queue is empty.
A is marked as it enters the queue: visited[A] = true, q = [A].
Poll A from the front of the queue and print it. Now scan its neighbors B, F, I.
u = B is unvisited → mark it and add it to the back of the queue (level 1).
u = F is unvisited → mark it and add it to the back of the queue (level 1).
u = I is unvisited → mark it and add it to the back of the queue (level 1).
Poll B from the front of the queue and print it. Now scan its neighbors A, C, E.
u = A: already visited → skip. Without the visited mark, A would enter the queue again.
u = C is unvisited → mark it and add it to the back of the queue (level 2).
u = E is unvisited → mark it and add it to the back of the queue (level 2).
Poll F from the front of the queue and print it. Now scan its neighbors A, G.
u = A: already visited → skip.
u = G is unvisited → mark it and add it to the back of the queue (level 2).
Poll I from the front of the queue and print it. Now scan its neighbor A.
u = A: already visited → skip.
Traversal (BFS / DFS) · Step through the code
Code Trace: bfs(A) (continued)
void bfs(int s) { boolean[] visited = new boolean[n]; Queue<Integer> q = new ArrayDeque<>(); visited[s] = true; q.add(s); while (!q.isEmpty()) { int w = q.poll(); System.out.print(w + " "); for (int u : adj.get(w)) if (!visited[u]) { visited[u] = true; q.add(u); } } }
Poll C from the front of the queue and print it. Now scan its neighbors B, D, E.
u = B: already visited → skip.
u = D is unvisited → mark it and add it to the back of the queue (level 3).
u = E: already visited → skip.
Poll E from the front of the queue and print it. Now scan its neighbors B, C, G.
u = B, C, G: all already visited → skip.
Poll G from the front of the queue and print it. Now scan its neighbors D, E, F.
u = D, E, F: all already visited → skip.
Poll D from the front of the queue and print it. Now scan its neighbors C, G, H.
u = C, G: all already visited → skip.
u = H is unvisited → mark it and add it to the back of the queue (level 4).
Poll H from the front of the queue and print it. Now scan its neighbor D.
u = D: already visited → skip.
The queue is empty → done. Order A B F I C E G D H: level by level. Each vertex enters the queue once and each list is scanned once → O(n + m).
Traversal (BFS / DFS)
Depth-First Search (DFS)
- Explores vertices along a path, going as deep as it can.
- At a dead end (no unvisited neighbors left) it backtracks and tries another branch.
- Uses a stack or recursive calls.
s→a→d→b goes deep; back up at the dead end, then c→e
Pros
- Usually uses little memory (only the current path is on the stack).
- Follows a path all the way to its end.
Con
- Does not guarantee shortest paths.
Traversal (BFS / DFS)
The DFS Algorithm
void dfs(int v, boolean[] visited) { visited[v] = true; System.out.print(v + " "); for (int u : adj.get(v)) if (!visited[u]) dfs(u, visited); }
Recursion keeps the stack for us, as the call stack.
void dfsStack(int s) { boolean[] visited = new boolean[n]; Deque<Integer> stack = new ArrayDeque<>(); stack.push(s); visited[s] = true; while (!stack.isEmpty()) { int v = stack.peek(); Integer next = null; for (int u : adj.get(v)) if (!visited[u]) { next = u; break; } if (next == null) stack.pop(); // back else { visited[next] = true; stack.push(next); } } }
Traversal (BFS / DFS) · Step through the code
Code Trace: dfs(A) (recursive)
void dfs(int v, boolean[] visited) { visited[v] = true; System.out.print(v + " "); for (int u : adj.get(v)) if (!visited[u]) dfs(u, visited); }
dfs(A, visited) is called (A=0, B=1, …, I=8; neighbors in alphabetical order). A is marked and printed.
Neighbor B of A is unvisited → call dfs(B): a new frame opens; B is marked and printed.
B's neighbor A is already visited → skip.
Neighbor C of B is unvisited → call dfs(C): a new frame opens; C is marked and printed.
C's neighbor B is already visited → skip.
Neighbor D of C is unvisited → call dfs(D): a new frame opens; D is marked and printed.
D's neighbor C is already visited → skip.
Neighbor G of D is unvisited → call dfs(G): a new frame opens; G is marked and printed.
G's neighbor D is already visited → skip.
Neighbor E of G is unvisited → call dfs(E): a new frame opens; E is marked and printed.
E's neighbors B, C, G are already visited → skip.
E has no neighbors left to visit → dfs(E) returns and its frame is popped (backtrack).
Neighbor F of G is unvisited → call dfs(F): a new frame opens; F is marked and printed.
F's neighbors A, G are already visited → skip.
F has no neighbors left to visit → dfs(F) returns and its frame is popped (backtrack).
Traversal (BFS / DFS) · Step through the code
Code Trace: dfs(A) (recursive, continued)
void dfs(int v, boolean[] visited) { visited[v] = true; System.out.print(v + " "); for (int u : adj.get(v)) if (!visited[u]) dfs(u, visited); }
G has no neighbors left to visit → dfs(G) returns and its frame is popped (backtrack).
Neighbor H of D is unvisited → call dfs(H): a new frame opens; H is marked and printed.
H's neighbor D is already visited → skip.
H has no neighbors left to visit → dfs(H) returns and its frame is popped (backtrack).
D has no neighbors left to visit → dfs(D) returns and its frame is popped (backtrack).
C's neighbor E is already visited → skip.
C has no neighbors left to visit → dfs(C) returns and its frame is popped (backtrack).
B's neighbor E is already visited → skip.
B has no neighbors left to visit → dfs(B) returns and its frame is popped (backtrack).
A's neighbor F is already visited → skip.
Neighbor I of A is unvisited → call dfs(I): a new frame opens; I is marked and printed.
I's neighbor A is already visited → skip.
I has no neighbors left to visit → dfs(I) returns and its frame is popped (backtrack).
dfs(A) returns and the stack is empty. Order: A B C D G E F H I.
Traversal (BFS / DFS) · Step through the code
Code Trace: dfsStack(A)
void dfsStack(int s) { boolean[] visited = new boolean[n]; Deque<Integer> stack = new ArrayDeque<>(); stack.push(s); visited[s] = true; while (!stack.isEmpty()) { int v = stack.peek(); Integer next = null; for (int u : adj.get(v)) if (!visited[u]) { next = u; break; } if (next == null) stack.pop(); // back else { visited[next] = true; stack.push(next); } } }
Start s = A. visited is all false; the stack is empty.
A is pushed and marked. The top of the stack = the vertex we are at.
peek = A; first unvisited neighbor B (break) → mark it and push it.
peek = B; first unvisited neighbor C (break) → mark it and push it.
peek = C; first unvisited neighbor D (break) → mark it and push it.
peek = D; first unvisited neighbor G (break) → mark it and push it.
peek = G; first unvisited neighbor E (break) → mark it and push it.
peek = E; no unvisited neighbor (next = null) → pop and back up.
peek = G; first unvisited neighbor F (break) → mark it and push it.
peek = F; no unvisited neighbor (next = null) → pop and back up.
peek = G; no unvisited neighbor (next = null) → pop and back up.
peek = D; first unvisited neighbor H (break) → mark it and push it.
peek = H; no unvisited neighbor (next = null) → pop and back up.
peek = D; no unvisited neighbor (next = null) → pop and back up.
peek = C; no unvisited neighbor (next = null) → pop and back up.
peek = B; no unvisited neighbor (next = null) → pop and back up.
peek = A; first unvisited neighbor I (break) → mark it and push it.
peek = I; no unvisited neighbor (next = null) → pop and back up.
peek = A; no unvisited neighbor (next = null) → pop and back up. The stack is empty: done. The visit order matches the recursive version.
Traversal (BFS / DFS)
BFS vs. DFS
| BFS | DFS | |
|---|---|---|
| Helper structure | Queue (FIFO) | Stack (LIFO) / recursion |
| Time (adjacency list) | O(n + m) | O(n + m) |
| Extra memory | O(n): the widest level is in the queue | O(n): the longest path is on the stack |
| Shortest paths? | Yes (in unweighted graphs) | No |
| Typical uses | fewest steps, finding levels | cycle detection, components, mazes |
The traces assume neighbors are visited in alphabetical order; a different neighbor order gives a different visit order. Because dfsStack rescans the neighbor list from the start on every peek, it can take longer than O(n + m); remembering where each vertex left off restores O(n + m).
Traversal (BFS / DFS) · Step through the code
Code Trace: Numbering Components with DFS
Every vertex is −1 (not yet in any component).
Component counter count = 0.
comp[0] = −1: new component. count++ yields 0 and count becomes 1; call dfs(0, 0).
comp[0] = 0: vertex 0 gets component number 0.
comp[1] = 0: vertex 1 gets component number 0.
Neighbor 0 is already labeled (comp = 0) → skip. Each edge is examined twice: O(n + m).
comp[2] = 0: vertex 2 gets component number 0.
Neighbor 0 is already labeled (comp = 0) → skip. Each edge is examined twice: O(n + m).
comp[1] = 0: vertex 1 was labeled by an earlier DFS → skip.
comp[2] = 0: vertex 2 was labeled by an earlier DFS → skip.
comp[3] = −1: new component. count++ yields 1 and count becomes 2; call dfs(3, 1).
comp[3] = 1: vertex 3 gets component number 1.
comp[4] = 1: vertex 4 gets component number 1.
Neighbor 3 is already labeled (comp = 1) → skip. Each edge is examined twice: O(n + m).
comp[4] = 1: vertex 4 was labeled by an earlier DFS → skip.
comp[5] = −1: new component. count++ yields 2 and count becomes 3; call dfs(5, 2).
comp[5] = 2: vertex 5 gets component number 2.
3 components: {0, 1, 2}, {3, 4}, {5}. Same comp = same component; the query comp[u] == comp[v] is O(1).
int components(int[] comp) { Arrays.fill(comp, -1); int count = 0; for (int s = 0; s < n; s++) if (comp[s] == -1) dfs(s, count++, comp); return count; } void dfs(int v, int c, int[] comp) { comp[v] = c; for (int u : adj.get(v)) if (comp[u] == -1) dfs(u, c, comp); }
Traversal (BFS / DFS)
Testing Whether a Graph Is Bipartite
- Bipartite: the vertices split into two groups, and every edge runs between the groups.
- Test = 2-coloring with BFS: give each neighbor the opposite color; two adjacent vertices of the same color → not bipartite.
- Equivalently: no odd-length cycle.
// color[] starts at -1; BFS from s color[s] = 0; q.add(s); while (!q.isEmpty()) { int w = q.poll(); for (int u : adj.get(w)) if (color[u] == -1) { color[u] = 1 - color[w]; q.add(u); } else if (color[u] == color[w]) return false; } return true;
Same cost as BFS: O(n + m). Uses: matching students to projects, two-shift scheduling, conflict-free grouping.
Traversal (BFS / DFS)
Topological Sorting: DAGs Only
- Orders the vertices of a digraph so that for every edge
u → v, u comes before v. - Possible only in directed acyclic graphs (DAGs).
- The result need not be unique: several vertices of in-degree 0 may be available at the same time.
- Uses: course prerequisites, task dependencies, build and workflow planning.
Kahn(G): q = vertices with inDegree 0 result = [] while q is not empty: u = remove from q append u to result for each edge (u, v) leaving u: inDegree[v]-- if inDegree[v] == 0: add v to q if result.size < n: there is a cycle return result
Traversal (BFS / DFS) · Step through the code
Code Trace: Topological Sorting (Kahn)
List<Integer> kahn(List<List<Integer>> adj) {
int n = adj.size();
int[] indeg = new int[n];
for (List<Integer> l : adj)
for (int v : l) indeg[v]++;
Queue<Integer> q = new ArrayDeque<>();
for (int v = 0; v < n; v++)
if (indeg[v] == 0) q.add(v);
List<Integer> result = new ArrayList<>();
while (!q.isEmpty()) {
int u = q.poll();
result.add(u);
for (int v : adj.get(u))
if (--indeg[v] == 0) q.add(v);
}
return result; // size < n → cycle
}
A course-prerequisite example: 6 vertices, 7 directed edges (u → v: u before v). indeg[] starts at all zeros.
Edges out of 0 (0→3): each target's in-degree goes up by one.
Edges out of 1 (1→3, 1→4): each target's in-degree goes up by one.
Edges out of 2 (2→0, 2→4): each target's in-degree goes up by one.
Edges out of 3 (3→5): each target's in-degree goes up by one.
Edges out of 4 (4→5): each target's in-degree goes up by one.
The vertices of in-degree 0 (1, 2) depend on nothing → enqueue them. (5's list is empty, so its counting round changed nothing.)
1 leaves the queue and joins the result: all its prerequisites are already there.
Count 1→3 as deleted: indeg[3] = 1; 3 still waits for another prerequisite.
Count 1→4 as deleted: indeg[4] = 1; 4 still waits for another prerequisite.
2 leaves the queue and joins the result: all its prerequisites are already there.
Traversal (BFS / DFS) · Step through the code
Code Trace: Topological Sorting (Kahn, continued)
List<Integer> kahn(List<List<Integer>> adj) {
int n = adj.size();
int[] indeg = new int[n];
for (List<Integer> l : adj)
for (int v : l) indeg[v]++;
Queue<Integer> q = new ArrayDeque<>();
for (int v = 0; v < n; v++)
if (indeg[v] == 0) q.add(v);
List<Integer> result = new ArrayList<>();
while (!q.isEmpty()) {
int u = q.poll();
result.add(u);
for (int v : adj.get(u))
if (--indeg[v] == 0) q.add(v);
}
return result; // size < n → cycle
}
Count 2→0 as deleted: indeg[0] = 0 → 0 is ready and joins the queue.
Count 2→4 as deleted: indeg[4] = 0 → 4 is ready and joins the queue.
0 leaves the queue and joins the result: all its prerequisites are already there.
Count 0→3 as deleted: indeg[3] = 0 → 3 is ready and joins the queue.
4 leaves the queue and joins the result: all its prerequisites are already there.
Count 4→5 as deleted: indeg[5] = 1; 5 still waits for another prerequisite.
3 leaves the queue and joins the result: all its prerequisites are already there.
Count 3→5 as deleted: indeg[5] = 0 → 5 is ready and joins the queue.
5 leaves the queue and joins the result: all its prerequisites are already there.
The queue is empty; the result is 1 2 0 4 3 5, and its size 6 = n → no cycle. Each edge caused one decrement: O(n + m).
Traversal (BFS / DFS) · Step through the code
Code Trace: Cycles in a Digraph (Three-Color DFS)
color[0] = 1 (gray): 0 is on the current DFS path.
Edge 0 → 1: 1 is white → recursive call.
color[1] = 1 (gray): 1 is on the current DFS path.
Edge 1 → 2: 2 is white → recursive call.
color[2] = 1 (gray): 2 is on the current DFS path.
2 has no outgoing edges: color[2] = 2 (black); return false and pop the frame.
1's neighbors are all done: color[1] = 2 (black); return false and pop the frame.
Edge 0 → 3: 3 is white → recursive call.
color[3] = 1 (gray): 3 is on the current DFS path.
Edge 3 → 2: 2 is black (finished). Not a cycle: there is no path from 2 back to 3. Only a gray vertex signals a cycle.
Edge 3 → 4: 4 is white → recursive call.
color[4] = 1 (gray): 4 is on the current DFS path.
Edge 4 → 0: 0 is gray, so it is on the path! This is a back edge → cycle 0 → 3 → 4 → 0 found.
The inner call returned true → 3 returns true too.
The inner call returned true → 0 returns true too.
// color: 0 white, 1 gray (on path), 2 black boolean hasCycle(int v, int[] color) { color[v] = 1; for (int u : adj.get(v)) { if (color[u] == 1) return true; // back edge if (color[u] == 0 && hasCycle(u, color)) return true; } color[v] = 2; return false; }
Shortest Paths
The Unweighted Shortest-Path Problem
- Find shortest paths from a source vertex to every other vertex (single-source shortest paths).
- Every edge is assumed to have the same weight (1): path length = number of edges.
- Solution: BFS. A vertex on level k is exactly k edges away.
Source 1; the number next to each vertex is its distance from 1
Shortest Paths
The Unweighted Shortest-Path Algorithm
dist[every v] = ∞ dist[s] = 0 queue.add(s) while queue is not empty: w = queue.remove() for each neighbor u of w: if dist[u] == ∞: dist[u] = dist[w] + 1 queue.add(u) return dist
"dist = ∞" also means "not visited", so no separate array is needed. In Java, graph maps each vertex to its neighbor list (Map<Integer, List<Integer>>), because the vertices are numbered from 1.
Map<Integer, Integer> shortestPath(int s) { final int INF = Integer.MAX_VALUE; // ∞ Map<Integer, Integer> dist = new HashMap<>(); for (int v : graph.keySet()) dist.put(v, INF); dist.put(s, 0); Queue<Integer> q = new ArrayDeque<>(); q.offer(s); while (!q.isEmpty()) { int w = q.poll(); for (int u : graph.get(w)) if (dist.get(u) == INF) { dist.put(u, dist.get(w) + 1); q.offer(u); } } return dist; }
Shortest Paths · Step through the code
Code Trace: shortestPath(1)
Map<Integer, Integer> shortestPath(int s) { final int INF = Integer.MAX_VALUE; // ∞ Map<Integer, Integer> dist = new HashMap<>(); for (int v : graph.keySet()) dist.put(v, INF); dist.put(s, 0); Queue<Integer> q = new ArrayDeque<>(); q.offer(s); while (!q.isEmpty()) { int w = q.poll(); for (int u : graph.get(w)) if (dist.get(u) == INF) { dist.put(u, dist.get(w) + 1); q.offer(u); } } return dist; }
Source s = 1. All distances are INF (∞): "not reached yet" = "not visited".
dist[1] = 0, and 1 enters the queue.
Dequeue 1; dist[1] = 0. Examine its neighbors 2, 3, 4.
dist[2] = INF → dist[2] = 0 + 1 = 1; enqueue 2.
dist[3] = INF → dist[3] = 0 + 1 = 1; enqueue 3.
dist[4] = INF → dist[4] = 0 + 1 = 1; enqueue 4.
Dequeue 2; dist[2] = 1. Examine its neighbors 1, 5.
1: distance already known (0) → unchanged. In BFS, the first distance found is the shortest.
dist[5] = INF → dist[5] = 1 + 1 = 2; enqueue 5.
Dequeue 3; dist[3] = 1. Examine its neighbors 1, 8.
1: distance already known (0) → unchanged. In BFS, the first distance found is the shortest.
dist[8] = INF → dist[8] = 1 + 1 = 2; enqueue 8.
Dequeue 4; dist[4] = 1. Examine its neighbors 1, 6.
Shortest Paths · Step through the code
Code Trace: shortestPath(1) (continued)
Map<Integer, Integer> shortestPath(int s) { final int INF = Integer.MAX_VALUE; // ∞ Map<Integer, Integer> dist = new HashMap<>(); for (int v : graph.keySet()) dist.put(v, INF); dist.put(s, 0); Queue<Integer> q = new ArrayDeque<>(); q.offer(s); while (!q.isEmpty()) { int w = q.poll(); for (int u : graph.get(w)) if (dist.get(u) == INF) { dist.put(u, dist.get(w) + 1); q.offer(u); } } return dist; }
1: distance already known (0) → unchanged. In BFS, the first distance found is the shortest.
dist[6] = INF → dist[6] = 1 + 1 = 2; enqueue 6.
Dequeue 5; dist[5] = 2. Examine its neighbors 2, 8.
2, 8: distance already known (1, 2) → unchanged. In BFS, the first distance found is the shortest.
Dequeue 8; dist[8] = 2. Examine its neighbors 3, 5, 7.
3, 5: distance already known (1, 2) → unchanged. In BFS, the first distance found is the shortest.
dist[7] = INF → dist[7] = 2 + 1 = 3; enqueue 7.
Dequeue 6; dist[6] = 2. Examine its neighbors 4, 7.
4, 7: distance already known (1, 3) → unchanged. In BFS, the first distance found is the shortest.
Dequeue 7; dist[7] = 3. Examine its neighbors 6, 8.
6, 8: distance already known (2, 2) → unchanged. In BFS, the first distance found is the shortest.
The queue is empty → return dist. The distance to 7 is 3 (1 → 3 → 8 → 7 was found; 1 → 4 → 6 → 7 also has 3 edges). O(n + m).
Shortest Paths
The Weighted Shortest-Path Problem
- Find shortest paths from a source vertex to every other vertex.
- Each edge may have its own weight: a distance, time, cost…
- Path length = sum of the edge weights.
Shortest path from A to F: A → C → E → D → F = 2 + 3 + 4 + 11 = 20
Shortest Paths
Dijkstra's Algorithm: Idea and Pseudocode
- Pick the unsettled vertex with the smallest distance.
- Update its neighbors' distances by relaxation.
- Use a priority queue for the selection and
prev[]to rebuild the path.
Dijkstra(G, s): for each vertex v: dist[v] = ∞ prev[v] = undefined dist[s] = 0 Q = all vertices // priority queue while Q is not empty: u = Q.removeMin() // smallest dist[u] for each neighbor v of u in Q: alt = dist[u] + weight(u, v) if alt < dist[v]: dist[v] = alt prev[v] = u // decrease v's priority in Q
Shortest Paths
Dijkstra's Algorithm in Java
long[] dijkstra(List<List<int[]>> adj, int s, int[] prev) { // weights ≥ 0 long[] d = new long[adj.size()]; Arrays.fill(d, Long.MAX_VALUE); d[s] = 0; Arrays.fill(prev, -1); PriorityQueue<long[]> pq = // {vertex, dist} new PriorityQueue<>((a, b) -> Long.compare(a[1], b[1])); pq.add(new long[]{s, 0}); while (!pq.isEmpty()) { long[] e = pq.poll(); int u = (int) e[0]; if (e[1] > d[u]) continue; // stale entry: skip for (int[] k : adj.get(u)) { int v = k[0]; long alt = d[u] + k[1]; if (alt < d[v]) { d[v] = alt; prev[v] = u; pq.add(new long[]{v, alt}); } } } return d; }
PriorityQueue has no "decrease key": we add a new entry, and the old one is skipped as stale when it comes out. prev[v] = u records the shortest-path tree; a path is traced back from the target.Shortest Paths · Step through the code
Code Trace: dijkstra(adj, A)
// d[] = ∞, d[s] = 0, prev[] = -1 pq.add(new long[]{s, 0}); while (!pq.isEmpty()) { long[] e = pq.poll(); int u = (int) e[0]; if (e[1] > d[u]) continue; // stale for (int[] k : adj.get(u)) { int v = k[0]; long alt = d[u] + k[1]; if (alt < d[v]) { d[v] = alt; prev[v] = u; pq.add(new long[]{v, alt}); } } }
Source A: d[A] = 0, all others ∞. Add {A, 0} to the queue. Priority = distance.
Poll the smallest entry {A, 0}; 0 = d[A] (current) → A is settled. Relax its neighbors B, F, I.
v = B: alt = 0 + 4 = 4 < ∞ → d[B] = 4, prev[B] = A, {B, 4} enters the queue.
v = F: alt = 0 + 7 = 7 < ∞ → d[F] = 7, prev[F] = A, {F, 7} enters the queue.
v = I: alt = 0 + 3 = 3 < ∞ → d[I] = 3, prev[I] = A, {I, 3} enters the queue.
Poll the smallest entry {I, 3}; 3 = d[I] (current) → I is settled. Relax its neighbor A.
v = A: alt = 6 ≥ d[A] = 0 → no update.
Poll the smallest entry {B, 4}; 4 = d[B] (current) → B is settled. Relax its neighbors A, C, E.
v = A: alt = 8 ≥ d[A] = 0 → no update.
v = C: alt = 4 + 1 = 5 < ∞ → d[C] = 5, prev[C] = B, {C, 5} enters the queue.
v = E: alt = 4 + 2 = 6 < ∞ → d[E] = 6, prev[E] = B, {E, 6} enters the queue.
Poll the smallest entry {C, 5}; 5 = d[C] (current) → C is settled. Relax its neighbors B, D, E.
v = B: alt = 6 ≥ d[B] = 4 → no update.
v = D: alt = 5 + 7 = 12 < ∞ → d[D] = 12, prev[D] = C, {D, 12} enters the queue.
v = E: alt = 8 ≥ d[E] = 6 → no update.
Shortest Paths · Step through the code
Code Trace: dijkstra(adj, A) (continued)
// d[] = ∞, d[s] = 0, prev[] = -1 pq.add(new long[]{s, 0}); while (!pq.isEmpty()) { long[] e = pq.poll(); int u = (int) e[0]; if (e[1] > d[u]) continue; // stale for (int[] k : adj.get(u)) { int v = k[0]; long alt = d[u] + k[1]; if (alt < d[v]) { d[v] = alt; prev[v] = u; pq.add(new long[]{v, alt}); } } }
Poll the smallest entry {E, 6}; 6 = d[E] (current) → E is settled. Relax its neighbors B, C, G.
v = B: alt = 8 ≥ d[B] = 4; v = C: alt = 9 ≥ d[C] = 5 → no update.
v = G: alt = 6 + 2 = 8 < ∞ → d[G] = 8, prev[G] = E, {G, 8} enters the queue.
Poll the smallest entry {F, 7}; 7 = d[F] (current) → F is settled. Relax its neighbors A, G.
v = A: alt = 14 ≥ d[A] = 0; v = G: alt = 9 ≥ d[G] = 8 → no update.
Poll the smallest entry {G, 8}; 8 = d[G] (current) → G is settled. Relax its neighbors D, E, F.
v = D: alt = 8 + 3 = 11 < 12 → d[D] = 11, prev[D] = G, {D, 11} enters the queue. The old entry {D, 12} stays in the queue as stale.
v = E: alt = 10 ≥ d[E] = 6; v = F: alt = 10 ≥ d[F] = 7 → no update.
Poll the smallest entry {D, 11}; 11 = d[D] (current) → D is settled. Relax its neighbors C, G, H.
v = C: alt = 18 ≥ d[C] = 5; v = G: alt = 14 ≥ d[G] = 8 → no update.
v = H: alt = 11 + 5 = 16 < ∞ → d[H] = 16, prev[H] = D, {H, 16} enters the queue.
{D, 12} comes out, but d[D] = 11 < 12: a stale entry (a better path was found later) → continue skips it.
Poll the smallest entry {H, 16}; 16 = d[H] (current) → H is settled. Relax its neighbor D.
v = D: alt = 21 ≥ d[D] = 11 → no update.
The queue is empty → done. Trace prev[] back: H ← D ← G ← E ← B ← A. The shortest path is A → B → E → G → D → H = 16.
Shortest Paths · Step through the code
Code Trace: Printing the Shortest Path with prev[]
The prev[] array Dijkstra leaves behind: for each vertex, the vertex just before it on its shortest path. List p starts empty.
Start at the target: v = F.
F goes to the front of the list (vertices added later come earlier on the path).
v = prev[…] = D: one step back.
D goes to the front of the list (vertices added later come earlier on the path).
v = prev[…] = E: one step back.
E goes to the front of the list (vertices added later come earlier on the path).
v = prev[…] = C: one step back.
C goes to the front of the list (vertices added later come earlier on the path).
v = prev[…] = A: one step back.
A goes to the front of the list (vertices added later come earlier on the path).
prev[A] = −1: we have reached the source, so the loop ends. 5 rounds = vertices on the path → O(path length).
Is the first element the source A? Yes → t is reachable. (For an unreachable t, the chain never reaches A and an empty list is returned.)
Result: A → C → E → D → F, length d[F] = 2 + 3 + 4 + 11 = 20.
List<Integer> path(int[] prev, int s, int t) { LinkedList<Integer> p = new LinkedList<>(); for (int v = t; v != -1; v = prev[v]) p.addFirst(v); if (p.getFirst() != s) return List.of(); return p; // s ... t }
Shortest Paths
Analyzing Dijkstra's Algorithm
| Implementation | Complexity | Why? |
|---|---|---|
| Priority queue (binary heap) | O((n + m) log n) | n remove-mins and at most m updates/inserts, each O(log n) |
| No queue (array scan) | O(n² + m) = O(n²) | Each step scans n vertices for the smallest distance; neighbor checks total m |
Minimum Spanning Trees
Minimum Spanning Trees
- Spanning tree: a subgraph of a connected, weighted graph that contains every vertex and no cycle; it has n − 1 edges for n vertices.
- Minimum spanning tree (MST): a spanning tree with the smallest total edge weight.
- Application: connecting cities with the least cable or pipe.
- Kruskal's algorithm answers "Does this edge form a cycle?" with Union-Find (disjoint sets):
find(x)returns the representative of x's set;union(u, v)merges two different sets and returnstrue, or returnsfalseif they are already the same set.
Minimum Spanning Trees
Kruskal's Algorithm
Kruskal(G): T = ∅ // chosen edges for each vertex v: makeSet(v) sort the edges by weight for each (u, v), lightest first: if find(u) != find(v): // no cycle T.add((u, v)) union(u, v) if |T| == n − 1: break return T // MST (if disconnected: forest)
- Greedy: always take the lightest edge that does not form a cycle.
- If u and v are already in the same set, the edge closes a cycle → skip it.
- Start with n disjoint sets; each accepted edge merges two of them.
- If the graph is disconnected, n−1 edges are never reached; the algorithm returns a minimum spanning forest, one tree per connected component.
| Sorting | O(m log m) |
| 2m find + (n−1) union | O(m · α(n)) |
| Total | O(m log n) |
Minimum Spanning Trees
Kruskal's Algorithm Step by Step
The edges are sorted by weight (left). Initially 10 vertices = 10 separate sets; T is empty.
I–J (0): find(I) ≠ find(J) → accept; the sets merge.
A–E (1): find(A) ≠ find(E) → accept; the sets merge.
C–I (1): find(C) ≠ find(I) → accept; the sets merge.
E–F (1): find(E) ≠ find(F) → accept; the sets merge.
G–H (1): find(G) ≠ find(H) → accept; the sets merge.
B–D (2): find(B) ≠ find(D) → accept; the sets merge.
C–J (2): C and J are already in the same set → it would form a cycle, so it is rejected.
D–E (2): find(D) ≠ find(E) → accept; the sets merge.
D–H (2): find(D) ≠ find(H) → accept; the sets merge.
A–D (4): A and D are already in the same set → it would form a cycle, so it is rejected.
B–C (4): find(B) ≠ find(C) → accept; the sets merge. 9 = n − 1 edges chosen: done, total weight 14.
Minimum spanning tree: A–E, E–F, D–E, B–D, D–H, G–H, B–C, C–I, I–J. The remaining edges need not be examined.
Minimum Spanning Trees · Step through the code
Code Trace: kruskal() + Union-Find
List<int[]> kruskal(int n, List<int[]> E) { E.sort((a, b) -> // {u, v, w} Integer.compare(a[2], b[2])); UnionFind uf = new UnionFind(n); List<int[]> T = new ArrayList<>(); for (int[] e : E) { if (uf.union(e[0], e[1])) // no cycle T.add(e); if (T.size() == n - 1) break; } return T; }
Sort the edges by weight (A=0 … J=9; sort is stable, so equal weights keep their input order). O(m log m).
new UnionFind(10): each vertex is its own set (10 roots). T is empty.
I–J (0): roots I ≠ J → union returns true: the trees merge (I → J) and the edge joins T.
A–E (1): roots A ≠ E → union returns true: the trees merge (A → E) and the edge joins T.
C–I (1): roots C ≠ J → union returns true: the trees merge (C → J) and the edge joins T.
E–F (1): roots E ≠ F → union returns true: the trees merge (F → E) and the edge joins T.
G–H (1): roots G ≠ H → union returns true: the trees merge (G → H) and the edge joins T.
B–D (2): roots B ≠ D → union returns true: the trees merge (B → D) and the edge joins T.
C–J (2): find(C) = find(J) = J → same tree; union returns false: the edge would close a cycle and is skipped.
D–E (2): roots D ≠ E → union returns true: the trees merge (D → E) and the edge joins T.
D–H (2): roots E ≠ H → union returns true: the trees merge (H → E) and the edge joins T.
A–D (4): find(A) = find(D) = E → same tree; union returns false: the edge would close a cycle and is skipped.
B–C (4): roots E ≠ J → union returns true: the trees merge (J → E) and the edge joins T. |T| = 9 = n − 1 → break.
return T: 9 edges, total weight 14. A single tree remains. The other 7 edges were never examined.
Minimum Spanning Trees
A Second Example: Airports
Goodrich's example: 9 airports, 19 flight routes (miles). Kruskal's order:
| 144, 184, 187, 337, 621 | accept |
| 740 ORD–JFK | reject (cycle) |
| 802 | accept |
| 849, 867 | reject |
| 946 | accept |
| 1090, 1121 | reject |
| 1235 LAX–DFW | accept → 8 edges |
Total: 4,456 miles.
Minimum Spanning Trees
Prim's Algorithm
Prim(G, s):
tree = {s}, T = ∅
add s's edges to the queue
while |T| < n − 1:
(u, v) = lightest edge // u in tree
if v in tree: skip // cycle
add v to tree, (u, v) to T
add v's edges leaving the tree
to the queue
return T
- The tree starts from a single vertex and grows by one vertex per step.
- Each step takes the lightest edge joining a tree vertex to a vertex outside the tree.
- The lightest edge leaving the tree always belongs to some minimum spanning tree: the tree must connect to the outside somehow, and this edge is the cheapest connection.
- Similar to Dijkstra; the difference is the queue key: the weight of a single edge, not the distance from the source.
- Unlike Kruskal, there is always a single tree along the way, so no Union-Find is needed.
Minimum Spanning Trees
Prim in Java (Priority Queue, Lazy Version)
List<int[]> prim(List<List<int[]>> adj, int s) { int n = adj.size(); // adj.get(u): {v, w} boolean[] inTree = new boolean[n]; PriorityQueue<int[]> pq = new PriorityQueue<>( (a, b) -> Integer.compare(a[2], b[2])); // {u, v, w} List<int[]> T = new ArrayList<>(); inTree[s] = true; for (int[] e : adj.get(s)) pq.add(new int[]{s, e[0], e[1]}); while (!pq.isEmpty() && T.size() < n - 1) { int[] e = pq.poll(); // lightest candidate if (inTree[e[1]]) continue; // stale: closes a cycle inTree[e[1]] = true; T.add(e); for (int[] f : adj.get(e[1])) if (!inTree[f[0]]) pq.add(new int[]{e[1], f[0], f[1]}); } return T; }
- The queue holds entries {u, v, w}: u is in the tree, v is a candidate.
- Lazy version: when v joins the tree, old entries leading to v are not removed; when polled, they are skipped with
continue(stale entries). - Each edge enters the queue at most once (from whichever end joins the tree first; when the other end joins, the first end is already in the tree): at most m inserts and removals, each O(log m).
- Total O(m log m) = O(m log n); extra memory O(m).
Minimum Spanning Trees
Prim's Algorithm Step by Step (from A)
Start: only A is in the tree. A's edges enter the queue: A–E 1, A–D 4, A–B 5. Ties are broken alphabetically.
A–E (1): lightest edge leaving the tree → E joins the tree. New candidates: E–F, E–D.
E–F (1): lightest edge leaving the tree → F joins the tree. New candidates: F–D, F–G.
E–D (2): lightest edge leaving the tree → D joins the tree. New candidates: D–B, D–H, D–G. Struck-out entries are now stale (both ends in the tree); they will be skipped when polled.
D–B (2): lightest edge leaving the tree → B joins the tree. New candidates: B–C.
D–H (2): lightest edge leaving the tree → H joins the tree. New candidates: H–G, H–C, H–I.
H–G (1): lightest edge leaving the tree → G joins the tree. New candidates: G–I. Struck-out entries are now stale (both ends in the tree); they will be skipped when polled.
B–C (4): lightest edge leaving the tree → C joins the tree. New candidates: C–I, C–J.
C–I (1): lightest edge leaving the tree → I joins the tree. New candidates: I–J.
I–J (0): lightest edge leaving the tree → J joins the tree. 9 = n − 1 edges: done, total weight 14.
Prim's tree: A–E, E–F, E–D, D–B, D–H, H–G, B–C, C–I, I–J. The same total weight (14) as Kruskal. If equal weights come out in another order (e.g. H–C instead of B–C), the tree may differ but still weighs 14.
Minimum Spanning Trees
Kruskal vs. Prim
| Kruskal | Prim | |
|---|---|---|
| Idea | tries edges from lightest to heaviest, merging trees | grows a single tree from one vertex |
| Along the way | a forest (several trees) | a single tree |
| Data structures | sorted edge list + Union-Find | priority queue + marker array, adjacency list |
| Cycle check | find(u) == find(v) | is v already in the tree? |
| Cost | O(m log n) (sorting dominates) | O(m log n) (with a heap); O(n²) with a matrix |
| When? | sparse graph, edges already in a list | adjacency list at hand; O(n²) version for very dense graphs |
Lab · Java code step by step
Lab: Minimum Spanning Trees
Summary
Summary
| Algorithm | Structure | Time (with a list) | What it gives |
|---|---|---|---|
| BFS | queue | O(n + m) | level order, unweighted shortest paths |
| DFS | stack / recursion | O(n + m) | depth-first traversal, cycles, components |
| Topological | queue / DFS | O(n + m) | dependency order for a DAG |
| Dijkstra | priority queue | O((n + m) log n) | weighted shortest paths (weights ≥ 0) |
| Kruskal | sorting + Union-Find | O(m log n) | minimum spanning tree |
| Prim | priority queue | O(m log n) | minimum spanning tree |
Summary
Interactive Simulators
Summary
Sample Problems
- At most how many edges can a simple undirected graph on 10 vertices have? If the degrees add up to 30, how many edges are there?
- Which representation would you use for a road network with n = 10,000 vertices and m = 30,000 edges? Why?
- Run BFS and DFS on the traversal example graph starting from H (neighbors in alphabetical order).
- If we write DFS with a queue instead of a stack, which algorithm do we get?
- In the weighted shortest-path example, if edge B → C weighed 1 instead of 5, what would the shortest path from A to D be?
- Why was edge C–J rejected in the Kruskal example?
- If Prim starts from J instead of A on the same graph, what are the first three edges chosen and the total weight?
Show answers
- 10·9/2 = 45 edges; Σ deg = 2m → m = 15.
- An adjacency list: O(n + m) ≈ 40,000 entries; a matrix would need 10⁸ cells, most of them 0.
- BFS: H D C G B E F A I · DFS: H D C B A F G E I.
- The visit order becomes BFS (the front of the queue is processed instead of the top of the stack).
- The path to C does not change (2 < 4 + 1); for D, A → C → E → D = 2 + 3 + 4 = 9, still 9.
- C–I and I–J had already been chosen; C and J are in the same set, so the edge would close the cycle C–I–J–C.
- J–I (0), I–C (1); C–J (2) comes out stale and is skipped; the third edge is C–B (4). The start vertex does not change the total: still 14.
Summary
Exercises
- Undirected edges (0,1), (0,2), (1,3), (2,3), (3,4), (4,5): what are the BFS order and distances from 0? (neighbors in increasing order)
- How many cells of this graph's adjacency matrix are 1? How many entries does its adjacency list hold in total?
- For the digraph 0→1, 1→2, 2→0, 2→3, while examining which edge does
hasCycle(0)return true? - How many topological orders does the DAG 0→1, 0→2, 1→3, 2→3 have? List them.
- If
prev = [-1, 0, 0, 1, 3], what is the path from 0 to 4? From 2 to 4? - Is a 5-vertex cycle (C5) bipartite? How many connected components does it have?
- In lazy Prim, at most how many entries enter the queue? When does an entry become stale?
Answers / hints
- Order 0 1 2 3 4 5; distances 0, 1, 1, 2, 3, 4.
- 2m = 12 cells (symmetric); the list also holds 2m = 12 entries.
- On edge 2 → 0: 0 is gray (on the path) → back edge.
- Two: 0 1 2 3 and 0 2 1 3.
- 0 → 1 → 3 → 4. The prev chain never passes through 2: it is the path tree built from source 0, not from 2.
- No (odd-length cycle). It is connected: 1 component.
- Each edge at most once: at most m entries. An edge is added only from whichever end joins the tree first; when the other end joins, the first end is already in the tree (the
!inTree[f[0]]check). An entry becomes stale once its target vertex joins the tree.
Summary
Further Reading
- Goodrich, Tamassia, and Goldwasser, Data Structures and Algorithms in Java, 6th ed., 2014.
- Cormen, Leiserson, Rivest, and Stein, Introduction to Algorithms, 4th ed., 2022.
- Sedgewick and Wayne, Algorithms, 4th ed., 2011.