Data Structures · Lecture Notes
Chapter 14: Graphs
Cities and the roads between them, people in a social network and their friendships, the courses in a department and their prerequisites: all three are described by the same abstract structure, a set of objects together with pairwise relationships among them. That structure is a graph. A tree gives every node one parent and forbids cycles; a graph drops both restrictions, so a vertex may be reachable along many routes, and a route may come back to where it started. We begin with the basic vocabulary of graphs, then look at how a graph is stored in memory and how that choice shapes the cost of each operation. Next come breadth-first and depth-first search, together with the algorithms built on them: finding components, testing bipartiteness, topological sorting, and cycle detection. The last two topics are shortest paths (BFS for unweighted graphs, Dijkstra for weighted ones) and minimum spanning trees (Kruskal and Prim).
Learning outcomes
- Basic concepts
- Explain vertices, edges, direction, weight, degree, paths, cycles, and connectivity; use the handshake lemma to count edges and degrees.
- Representations
- Store a graph as an edge list, an adjacency list, or an adjacency matrix; compare their memory and time costs and pick the one that suits a problem.
- Traversal and ordering
- Carry out BFS and DFS by hand and in code; find connected components, test bipartiteness, topologically sort a directed acyclic graph (DAG), and detect directed cycles.
- Shortest paths and MSTs
- Apply BFS to unweighted shortest paths, Dijkstra's algorithm to weighted ones, and Kruskal's and Prim's algorithms to minimum spanning trees, and justify which one a problem calls for.
14.1Basic concepts
Graph algorithms speak a small vocabulary: vertex, edge, degree, path, cycle, connectivity. The words mean roughly what they do in everyday speech, but each has a precise definition, and correctness arguments rest on it.
14.1.1What is a graph?
A city map shows intersections, the roads between them, and much detail besides: lengths, slopes, lanes. To answer “which intersection can I reach from which?” two facts suffice: the intersections, and which pairs are joined directly by a road. The abstract structure built from these two facts is a graph.
A graph is a pair G = (V, E), where V is a finite set of vertices (or nodes) and E is a set of edges. Each edge joins two vertices and is written (u, v). We write n = |V| for the number of vertices and m = |E| for the number of edges.
|V| is the size of V. Throughout, n counts vertices, m counts edges, and costs are stated in these terms: “O(n + m) time” grows with vertices plus edges.
In set notation, the graph of Figure 14.1 is:
- V = {0, 1, 2, 3, 4}, so n = 5.
- E = {(0,1), (0,4), (1,2), (1,3), (1,4), (2,3), (3,4)}, so m = 7.
The edges have no direction: (0,1) and (1,0) are one edge, listed once. Picture and sets carry the same information; vertex positions and line shapes are not part of the graph.
A graph generalizes a tree, which has a root, one parent per non-root node, and no cycles. A graph requires none of this:
- No root. No vertex is special; an algorithm must be told where to start.
- Cycles. Edges can lead back to the start; in Figure 14.1, 1 → 2 → 3 → 1 is one.
- Several routes. From 0 to 3 you can go 0 → 1 → 3 or 0 → 4 → 3.
A tree models one-to-many relationships; a graph models many-to-many ones. A person has many friends, each with many friends; a course has several prerequisites and is a prerequisite of several others. The algorithms pay for this flexibility: they may reach a vertex twice and must notice.
Mistaking a drawing of a graph for the graph. The same graph can be drawn in many ways; moving vertices or letting two edges cross creates no new vertex or edge, and a crossing point is not a vertex. A graph is defined by its sets V and E alone.
14.1.2Vertices and edges
A vertex stands for a modeled object: a city, a person, a computer, a web page, a course. It may carry a label, name, or position, but algorithms prefer vertices numbered 0 to n − 1, so per-vertex data can live in arrays indexed by vertex.
Modeled as a graph, “what is the shortest route from Istanbul to Van?” becomes a graph algorithm problem; with road lengths on the edges, Dijkstra's algorithm solves it (14.4.4). Modeling keeps the essence and drops the rest: paving is irrelevant to the shortest route, length is not.
An edge stands for a relationship: a road, a network link, a friendship. If (v, w) is an edge, v and w are adjacent, each a neighbor of the other, and the edge is incident to both. In Figure 14.1, 1 and 3 are neighbors; 0 and 2 are not.
Figure 14.3 shows a typical trait of real networks: many vertices, few neighbors each. Millions of members, but at most a few thousand friends per person, so the edges are a tiny fraction of those possible. Such graphs are sparse, and sparsity decides how to store them (14.2.6).
14.1.3Degree and the handshake lemma
In an undirected graph, the degree of a vertex v, written deg(v), is the number of edges incident to v. A vertex of degree 0 is an isolated vertex. An edge joining a vertex to itself is a self-loop, and it contributes 2 to the degree.
An intersection's degree is the number of roads meeting there. A road that leaves an intersection and returns to it is attached at both ends, which is why a self-loop counts twice.
The degrees in Figure 14.4 sum to 1 + 3 + 3 + 2 + 5 + 2 + 0 = 16, and the graph has seven ordinary edges plus a self-loop, eight in all. The sum being exactly twice the edge count is no accident.
In every undirected graph, the degrees sum to twice the number of edges: Σ deg(v) = 2m.
Ask how often each edge is counted. An edge (u, v) adds 1 to deg(u) and 1 to deg(v) and touches no other degree; a self-loop has u = v and by definition also adds 2. Every edge adds exactly 2, so m edges add 2m. □
At a party, the numbers of hands each guest shook add up to twice the number of handshakes, since each handshake involves two hands. Two consequences:
- The degrees give the edge count: m = Σ deg(v) / 2. Degrees summing to 30 mean 15 edges.
- The sum is even, so the number of odd-degree vertices is even. No graph has degrees 3, 3, 2, 2, 1: their sum is 11.
Forgetting to divide the degree sum by two. Each edge is counted once at each end, so the sum is twice the number of edges. The same slip appears in memory estimates: the adjacency lists of an undirected graph hold 2m entries, not m.
14.1.4Directed graphs
Friendship is symmetric, and a two-way road runs both ways. Other relationships go one way: a web page may link to one that does not link back, a prerequisite does not work in reverse, a one-way street runs one way. These are modeled by a directed graph, or digraph.
In a directed graph, each edge is an ordered pair (u, v), drawn as an arrow from u to v. u is the edge's tail (its origin) and v its head (its destination); (u, v) ≠ (v, u). The in-degree of a vertex is the number of arrows entering it, and its out-degree the number leaving it.
| Vertex | A | B | C | D | E | F | Total |
|---|---|---|---|---|---|---|---|
| in-degree | 0 | 2 | 1 | 1 | 1 | 1 | 6 |
| out-degree | 1 | 1 | 1 | 1 | 2 | 0 | 6 |
Count the arrows: two enter B (from A and D), so its in-degree is 2; two leave E (to D and F), so its out-degree is 2. Nothing enters A or leaves F.
Both rows sum to 6, the number of edges: each arrow adds 1 to its tail's out-degree and 1 to its head's in-degree, so Σ in-degree = Σ out-degree = m. In directed graphs, the undirected 2m splits into two sums.
Reachability depends on direction too. In Figure 14.5 A reaches B, but nothing reaches A. From B you can go to C, E, D, and back to B, so B, C, E, and D reach one another, while A and F hang off this group.
Treating directed adjacency as two-way. An edge (u, v) makes v a neighbor of u, but without (v, u), u is not a neighbor of v. That does not make u unreachable from v: an indirect path may exist.
14.1.5Paths and cycles
A path is a vertex sequence v0, v1, …, vk in which each (vi, vi+1) is an edge. A path repeating no vertex is a simple path. A path that ends where it starts is a cycle; it is a simple cycle if no vertex repeats except the start at the end.
In an unweighted graph a path's length is its number of edges: k + 1 vertices, length k. A path that follows the arrows of a directed graph is a directed path; such a cycle is a directed cycle.
Both cycles in Figure 14.6 have length six, and there are others: C → G → L → C uses the edges C–G, G–L, and L–C. The number of cycles grows fast with the number of edges.
In a tree, exactly one simple path joins any two nodes. In a graph there may be several, and two different simple paths together form a cycle. Istanbul → Ankara → Izmir → Istanbul is a simple cycle of flights: no city is visited twice. Istanbul → Ankara → Izmir → Ankara → Istanbul passes Ankara twice, so it is a cycle but not a simple one.
14.1.6Two example graphs
Here are the concepts at work on a small undirected graph and a small directed one.
For the graph of Figure 14.7:
- Adjacency. 1 and 2 are neighbors because (1,2) is an edge; 2 and 3 are not.
- Degrees. 1 has neighbors 2, 3, 4 and 4 has 1, 3, 5, so deg(1) = deg(4) = 3; 2, 3, and 5 have two each. The sum 3 + 2 + 2 + 3 + 2 = 12 = 2 · 6 confirms the lemma.
- Kinds of path. The table below classifies four sequences.
| Sequence | Kind | Reason |
|---|---|---|
| 1 → 2 → 5 | simple path | edges 1–2, 2–5 exist; no repeats |
| 1 → 3 → 4 → 1 | simple cycle | ends at 1; no repeats between |
| 1 → 3 → 4 → 1 → 2 | non-simple path | all edges exist; 1 appears twice |
| 1 → 3 → 4 → 1 → 4 → 1 | non-simple cycle | ends at 1; 1 and 4 repeat between |
A sequence is a path if each consecutive pair is an edge; 2 → 5 → 3 is not, since 5–3 is missing. Only then do we check simplicity: does a vertex repeat?
In the directed graph of Figure 14.8:
- 2 is a neighbor of 1 (arrow 1 → 2), but 1 is not a neighbor of 2 (no arrow 2 → 1).
- 1 → 2 → 5 is a directed path.
- 1 → 4 → 3 → 1 is a directed cycle (arrows 1 → 4, 4 → 3, 3 → 1).
- No arrow leaves 5, so no other vertex is reachable from it.
14.1.7Connected and complete graphs
An undirected graph is connected if there is a path between every two distinct vertices. A largest group of vertices that can all reach one another is a connected component. A graph is complete if every two distinct vertices are joined directly by an edge; the complete graph on n vertices is written Kn.
Connectivity asks for a path, completeness for an edge: a complete graph is connected, not necessarily vice versa. “Largest” matters: a component includes every vertex that can reach any of its members. Components split the vertices into disjoint groups; a connected graph has one.
The edge count has two bounds. In a simple undirected graph (no self-loops or parallel edges) each vertex pair has at most one edge, and there are n(n − 1)/2 pairs: n choices, then n − 1, halved because each pair is counted twice. So m ≤ n(n − 1)/2, with equality only when complete: K5 has 5 · 4 / 2 = 10 edges, the complete graph on 10 vertices 10 · 9 / 2 = 45.
A connected graph has m ≥ n − 1. Add edges one by one to an empty graph: it starts with n components, and each edge either merges two or falls inside one, so reaching one component takes at least n − 1 edges. A connected graph with exactly n − 1 edges is a tree, as we will see again with spanning trees (14.5.1).
These bounds separate sparse from dense graphs: m ≈ n is sparse, m ≈ n2 dense. Road and social networks are sparse; small groups where everyone knows everyone are dense.
14.1.8Weighted graphs
Often a connection has a cost: a road's length, a link's latency, a pipeline's price, a line's capacity.
In a weighted graph, each edge is assigned a number called its weight. The length or weight of a path is the sum of the weights of its edges. A weighted graph may be undirected or directed.
In the undirected graph of Figure 14.10, the path 3 → 4 → 1 has length 5 + 3 = 8; in the directed graph, 1 → 4 → 3 has length 3 + 5 = 8.
The direct edge 1–3 weighs 10, yet 1 → 4 → 3 costs 8. A direct edge need not be the shortest path; more edges can be shorter if they are light.
Hence shortest-path algorithms. Without weights the shortest path has the fewest edges; with weights, edge counts mislead and the weight sum decides. An unweighted graph is a weighted one with all weights 1.
14.1.9The graph ADT
A graph can first be defined as an abstract data type (ADT): which operations exist and what they do, not how memory carries them out. The Graph interface of Goodrich, Tamassia, and Goldwasser has these basic operations.
| Operation | Description |
|---|---|
numVertices(), numEdges() | Number of vertices n, number of edges m |
vertices(), edges() | Iteration over all vertices / all edges |
getEdge(u, v) | Returns the edge from u to v if there is one, otherwise null |
endVertices(e), opposite(v, e) | The two endpoints of an edge / the endpoint 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) | Adds a new vertex / a new edge (x: the value stored) |
removeVertex(v), removeEdge(e) | Removes a vertex with its incident edges / removes an edge |
Algorithms lean on two of these: getEdge(u, v) (are u and v adjacent?) and outgoingEdges(v) (who are v's neighbors?). BFS, DFS, Dijkstra, and Prim call the second once per vertex, so how fast a representation lists neighbors sets their running time.
removeVertex(v) removes the incident edges for consistency: an edge without an endpoint is meaningless. The cost of each operation depends on the representation.
14.2Graph representations
A graph can be stored in three forms, each speeding up some operations and slowing others. We take them in turn, compare their costs, and trace how Java builds an adjacency list and matrix.
14.2.1Three representations; the edge list
The three basic ways to store a graph are:
- Edge list: all edges in a single list, such as
[(A,B), (A,E), …]. - Adjacency list: for each vertex, a list of its neighbors, such as
V[i] = {j, k, l}. - Adjacency matrix: an n × n array whose cell
M[i][j]records whether there is an edge from i to j.
The edge list is the simplest: for Figure 14.11 it is (A, B) (A, E) (B, C) (B, E) (C, D) (D, E). A separate vertex list keeps edgeless vertices, for O(n + m) memory. Adding an edge appends one item: O(1).
Queries are the weakness. Testing adjacency means searching the unordered list for (u, v) or (v, u), O(m) in the worst case, and finding u's neighbors means scanning it all, since u's edges can be anywhere. Traversals ask this for every vertex, so they cost O(n · m). Kruskal's algorithm, which sorts edges by weight and never asks for neighbors, is where the edge list shines (14.5.3).
14.2.2The adjacency matrix
The adjacency matrix of a graph whose vertices are numbered 0 to n − 1 is an n × n array matrix with matrix[i][j] = 1 if there is an edge from i to j and 0 otherwise. In a weighted graph the cell holds the edge's weight, and a missing edge is marked with ∞ or some special value meaning the same.
An undirected edge (i, j) goes into both matrix[i][j] and matrix[j][i], so the matrix is symmetric about the diagonal: matrix[i][j] == matrix[j][i]. A directed graph's matrix is not.
m[1][2] takes a single access, O(1); finding the neighbors of 2 means reading all of row 2, O(n).The matrix's strength is direct access: a Java array cell is read or written in constant time, so three operations are O(1):
- Testing for edge i–j reads
matrix[i][j]. - Adding an edge writes 1 into one cell (two if undirected).
- Removing it writes 0 there.
It has two weaknesses. Listing neighbors scans all n cells of a row, O(n) even for a single neighbor. And the matrix takes n2 cells regardless of edges; in a sparse graph nearly all are 0, memory spent on edges that do not exist.
A road network of a million vertices (n = 106) needs n2 = 1012 matrix cells, about 125 GB even at one bit each. But three or four roads meet at an intersection, so there are a few million edges: a few filled cells per million.
14.2.3Adjacency matrices: three examples
Here are the matrices of three graphs on the vertices A–E: undirected, directed, and weighted. Rows and columns run A, B, C, D, E.
| 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 |
Each undirected edge fills two cells: six edges, twelve 1s, symmetric about the diagonal (row A, column E and row E, column A are both 1). Row sums are degrees: row B has three 1s (A, C, E), so deg(B) = 3. Without self-loops, the diagonal is 0.
| 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 |
Each arrow fills one cell: nine arrows, nine 1s. Rows are sources and columns destinations, so there is no symmetry: A → B sets m[A][B] = 1, but m[B][A] = 0. Row sums are out-degrees, column sums in-degrees: D's out-degree is 3; row C is all 0s, since nothing leaves C; column C has two 1s (from B and D), so C's in-degree is 2.
| A | B | C | D | E | |
|---|---|---|---|---|---|
| A | 0 | 4 | ∞ | ∞ | ∞ |
| B | ∞ | 0 | 3 | 3 | ∞ |
| C | 2 | ∞ | 0 | ∞ | 2 |
| D | ∞ | ∞ | ∞ | 0 | ∞ |
| E | ∞ | 5 | ∞ | 2 | 0 |
Weighted cells hold weights: the 4 at row A, column B weighs A → B. Missing edges hold ∞; the diagonal holds 0, a vertex's distance to itself. This suits shortest paths: ∞ means “no direct route” and behaves in sums, since ∞ plus anything is ∞.
Writing 0 for missing edges in a weighted matrix. A 0 looks like a real edge of weight 0, so the algorithm takes a free ride that does not exist. Java starts every int[][] cell at 0, so the mistake happens by default; fill edgeless cells with a value such as Integer.MAX_VALUE. Diagonal 0s are correct.
14.2.4The adjacency list
An adjacency list keeps one list per vertex, n lists in all. list[i] holds the neighbors of i (with their weights, in a weighted graph). The lists are usually linked lists or dynamic arrays.
An undirected edge (v, w) goes into two lists: w into v's and v into w's. A directed edge goes only into its source's list. Since only existing edges are stored, memory is n list heads plus the entries: 2m entries if undirected, m if directed, O(n + m) either way.
Lists are strong at traversal: visiting a vertex's neighbors reads its list in O(deg(v)), never touching non-neighbors as the matrix does. Adding an edge appends: amortized O(1) for a dynamic array, O(1) for a linked list.
They are weak at edge queries. Unless sorted, u's list must be scanned for v: O(deg(u)), or O(min(deg u, deg v)) if an undirected graph scans the shorter list. For high-degree vertices this is slow next to the matrix's O(1).
14.2.5Adjacency lists: three examples
Here are the adjacency lists of the three graphs of Figure 14.13, neighbors in alphabetical order.
- Undirected. Six edges make 12 entries, each edge written twice. A list's length is the vertex's degree: B's list has three items, and deg(B) = 3.
- Directed. Only outgoing edges: nine arrows, nine entries; C's list is empty. Length is the out-degree; in-degrees need a scan of every list, a drawback for directed graphs.
- Weighted. Entries are (neighbor, weight) pairs; B's list is
(C, 3), (D, 3). Java uses anint[]{v, w}or a smallEdgeclass.
14.2.6What each representation costs
| Operation | Edge list | Adjacency list | Adjacency matrix |
|---|---|---|---|
| Memory | O(n + m) | O(n + m) | O(n2) |
Edge u–v? (getEdge) | O(m) | undirected: O(min(deg u, deg v)); directed u → v: O(outDeg u) | O(1) |
| Visit v's neighbors | O(m) | O(deg v) | O(n) |
| Add edge | O(1) | O(1) | O(1) |
Choose by the graph's structure and its most frequent operation:
- Sparse graphs (m ≈ n), like road and social networks: a matrix's n2 cells sit nearly empty, while a list keeps memory at O(n + m) and speeds traversal. Every traversal and shortest-path algorithm here runs in or near O(n + m) with lists.
- Dense graphs (m ≈ n2): few cells are wasted, and frequent edge queries favor the matrix's O(1).
Which representation suits a road network with n = 10,000 vertices and m = 30,000 edges?
- Matrix. n2 = 108 cells, of which 2m = 60,000 are filled in an undirected graph: 60,000 / 108 = 0.06%.
- List. n heads and 2m entries: 10,000 + 60,000 = 70,000 items, the same order as n + m = 40,000, so memory is O(n + m).
- Decision. The graph is sparse (m = 3n): choose the adjacency list. Traversal, too, takes O(n + m) with the list and O(n2) with the matrix.
Confusing the matrix's row scan with getEdge. In a matrix, testing for an edge reads one cell, O(1); what costs O(n) is scanning a row for all neighbors. Lists are the other way around: visiting neighbors is cheap, querying one edge relatively expensive.
14.2.7An adjacency list in Java
With vertices numbered 0 to n − 1, an adjacency list is a list of lists: item i of the outer list is the neighbor list of vertex i.
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 directed graph
}
List<Integer> neighbors(int u) { return adj.get(u); } // O(1)
int degree(int u) { return adj.get(u).size(); }
}adjis aList<List<Integer>>: one innerArrayList(dynamic array) per vertex.- The constructor stores n and adds n empty lists in O(n). All vertices are known up front, so
adj.get(u)is valid for every u. addEdge(u, v)puts v in u's list and u in v's; a directed graph drops the second line. LikeArrayList.add, it is amortized O(1).neighbors(u)returns the list itself, uncopied, in O(1); iterating over it costs the caller O(deg(u)).degree(u)is the list's size, O(1) sinceArrayListstores it.
The matrix version is shorter: boolean[][] m = new boolean[n][n]; creates it, m[u][v] = m[v][u] = true; adds an edge, m[u][v] queries. With weights, a list item becomes an int[]{v, w} or a small Edge class, and the matrix an int[][] with Integer.MAX_VALUE for missing edges.
14.2.8Code trace: building an adjacency list
We build a five-vertex graph from six edges, in the order {0,1}, {0,4}, {1,2}, {1,4}, {2,3}, {3,4}. The traced code is the constructor, addEdge, and the main fragment that calls them.
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 if directed
}
// 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]);| Step | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
constructor (i = 0 … 4) | [] | [] | [] | [] | [] |
addEdge(0, 1) | [1] | [0] | [] | [] | [] |
addEdge(0, 4) | [1, 4] | [0] | [] | [] | [0] |
addEdge(1, 2) | [1, 4] | [0, 2] | [1] | [] | [0] |
addEdge(1, 4) | [1, 4] | [0, 2, 4] | [1] | [] | [0, 1] |
addEdge(2, 3) | [1, 4] | [0, 2, 4] | [1, 3] | [2] | [0, 1] |
addEdge(3, 4) | [1, 4] | [0, 2, 4] | [1, 3] | [2, 4] | [0, 1, 3] |
First the constructor: new Graph(5) sets n = 5, and the loop runs i = 0 to 4, giving each vertex an empty list.
Then the edges. addEdge(0, 1) appends 1 to 0's list, then 0 to 1's; every call does the same. Lists keep insertion order: 1's list is [0, 2, 4] because 0–1, 1–2, and 1–4 arrived in that order.
addEdge(0, 1): 0: [1], 1: [0]. (c) After addEdge(1, 4): 1: [0, 2, 4], 4: [0, 1]. (d) Final state: 12 entries.Six edges produce 2 · 6 = 12 entries. The list lengths 2, 3, 2, 2, 3 sum to 12, the handshake lemma as it shows up in code; degree(1) is the size of 1's list, 3. Each addEdge takes constant time, and memory is O(n + m).
Dropping the second line of addEdge in an undirected graph. The graph silently turns directed: 0–1 is in 0's list but not 1's, so a traversal from 1 never reaches 0. No error, just wrong answers. Conversely, adding a one-way relationship both ways invents return routes.
14.2.9Code trace: building an adjacency matrix
Now the same six edges go into a 5 × 5 boolean matrix.
boolean[][] m = new boolean[n][n];
void addEdge(int u, int v) {
m[u][v] = true;
m[v][u] = true; // not in a directed graph
}
// main: the same 6 edges (n = 5)
for (int[] e : E) g.addEdge(e[0], e[1]);new boolean[n][n] allocates 25 cells, which Java initializes to false: no edges yet. Even creating the matrix takes O(n2), since every cell must receive its initial value.
Each addEdge(u, v) writes two cells. addEdge(0, 1) sets m[0][1] = true (row 0, column 1) and then the symmetric cell m[1][0]. The six edges fill the cell pairs (0,1)–(1,0), (0,4)–(4,0), (1,2)–(2,1), (1,4)–(4,1), (2,3)–(3,2), (3,4)–(4,3).
true, 13 false, symmetric about the diagonal.| 0 | 1 | 2 | 3 | 4 | |
|---|---|---|---|---|---|
| 0 | 0 | 1 | 0 | 0 | 1 |
| 1 | 1 | 0 | 1 | 0 | 1 |
| 2 | 0 | 1 | 0 | 1 | 0 |
| 3 | 0 | 0 | 1 | 0 | 1 |
| 4 | 1 | 1 | 0 | 1 | 0 |
Even here the difference shows. “Edge 2–4?” is one access to m[2][4] (false); the list must scan [1, 3]. But 2's neighbors cost five cells in the matrix and two items in the list. Thirteen of 25 cells carry nothing, and the ratio worsens fast as graphs grow and thin out.
14.3Traversal: BFS and DFS
Most graph algorithms rest on a systematic exploration of the graph: breadth-first or depth-first search. Both visit the same vertices, in orders that suit different problems. Four applications follow: connected components, bipartiteness, topological sorting, and directed cycle detection.
14.3.1Traversal and visited marks
A traversal visits every vertex reachable from a starting vertex, each exactly once, in a systematic order.
There are two basic methods:
- Breadth-first search (BFS) visits all neighbors of the start, then their neighbors, level by level, using a queue (FIFO: first in, first out).
- Depth-first search (DFS) goes as deep as it can along one path, backs up at a dead end, and tries another branch, using a stack (LIFO: last in, first out) or recursion.
In a tree one path leads from the root to each node, so descending to children never reaches a node twice. A graph may have cycles and several routes to a vertex, so its traversal must record what it has seen, usually in a boolean[] visited array indexed by vertex.
Running a tree traversal on a graph unchanged. Without visited marks, a cycle traps the traversal forever; even without cycles, a vertex reached by several routes is processed repeatedly, and the cost can grow exponentially. The mark is part of the algorithm's correctness, not a detail.
14.3.2Breadth-first search
Drop a stone into water and the ripples spread in rings, near ones first, then farther ones. BFS works the same way: it finds the vertices one edge from the start, then those two edges away, then three. The vertices k edges from the start form level k.
A queue enforces this order: discovered vertices join the back, and processing takes from the front. All of level 1 enters before any of level 2, so all of level 1 is processed first.
The edges by which BFS discovers vertices form the BFS tree. In Figure 14.18, d is reachable from a or b; a comes first, so d enters along a–d and b–d stays out. Tree paths from the root have the fewest possible edges, the basis of unweighted shortest paths (14.4.1).
BFS finds unweighted shortest paths and discovers near vertices first: “friends of friends” is level 2, and a web crawler scans a site layer by layer. Its weakness is memory: a whole level may wait in the queue, which grows large in wide graphs.
14.3.3The BFS algorithm and code
First the pseudocode, then the Java.
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)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);
}
}
}The method belongs to the Graph class of 14.2.7 and uses its fields n and adj:
boolean[] visited = new boolean[n];allocates one mark per vertex, allfalse.Queue<Integer> q = new ArrayDeque<>();creates an empty queue;addappends at the back,pollremoves the front, both O(1).visited[s] = true; q.add(s);marks and enqueues the start.- Until the queue empties, each round removes and prints the front vertex
w. - The inner loop marks and enqueues each unvisited neighbor
uofw.
The subtle point is when to mark: on entering the queue, not on leaving. A vertex on several lists then enters once; marking on removal would let another neighbor enqueue it again while it waits.
Marking on dequeue. On the triangle 0–1, 0–2, 1–2, such a BFS from 0 prints 0 1 2 2: removing 1 enqueues the still unmarked 2 a second time. In dense graphs the queue grows to O(m) instead of O(n).
Cost. Each vertex enters and leaves the queue once, O(n), and its list is scanned once; the lists total 2m entries, O(m). So BFS takes O(n + m) with lists and O(n2) with a matrix, whose rows have n cells. visited and the queue take O(n) extra memory.
14.3.4Code trace: bfs(A)
We trace bfs(A) on a graph with nine vertices, numbered A = 0, B = 1, …, I = 8, and 11 edges. Neighbor lists are alphabetical: A: B, F, I · B: A, C, E · C: B, D, E · D: C, G, H · E: B, C, G · F: A, G · G: D, E, F · H: D · I: A.
visited[] entry.| Round | Removed w (printed) | Neighbors scanned | Marked and enqueued | Queue (end of round, front to back) |
|---|---|---|---|---|
| start | — | — | A | [A] |
| 1 | A | B, F, I | B, F, I (level 1) | [B, F, I] |
| 2 | B | A, C, E | C, E (level 2) | [F, I, C, E] |
| 3 | F | A, G | G (level 2) | [I, C, E, G] |
| 4 | I | A | — | [C, E, G] |
| 5 | C | B, D, E | D (level 3) | [E, G, D] |
| 6 | E | B, C, G | — | [G, D] |
| 7 | G | D, E, F | — | [D] |
| 8 | D | C, G, H | H (level 4) | [H] |
| 9 | H | D | — | [] |
A is marked and enqueued. Round 1 removes and prints A and enqueues its unvisited neighbors B, F, I: level 1.
Round 2 removes B. Its neighbor A is marked and skipped (without the mark, A would re-enter the queue); C and E join the back, giving [F, I, C, E], level 1 in front, level 2 behind. The queue keeps levels apart by itself.
Round 3 removes F and adds G; round 4 removes I and adds nothing, ending level 1. Round 5 removes C and adds D (level 3). E and G find every neighbor marked. Round 8 has D add H (level 4), and removing H empties the queue.
bfs(A). (a) A processed; queue B, F, I. (b) B processed; queue F, I, C, E. (c) C processed; queue E, G, D. (d) Final state: all vertices processed, bold edges forming the BFS tree.The output A B F I C E G D H shows the levels as consecutive blocks: {A}, {B, F, I}, {C, E, G}, {D}, {H}. The BFS tree has the eight edges A–B, A–F, A–I, B–C, B–E, F–G, C–D, D–H, as nine vertices require. Each vertex was enqueued once; the scans covered 2m = 22 entries.
14.3.5Depth-first search
In a maze, you walk a corridor to its end; at a dead end you return to the last fork and try a new corridor. That is DFS: it goes as deep as it can, and at a vertex with no unvisited neighbors it backtracks to try another branch.
Compare Figure 14.18: same graph, different order. BFS visits all of s's neighbors a, b, c next; DFS descends s, a, d, b, finds b's neighbors all seen, backs up to s, and descends to c, then e.
DFS stores only the current path, so on wide, shallow graphs it needs less memory than BFS, and it suits problems where a path is followed to its end (mazes, game trees). But its paths need not be shortest: it reached b by s → a → d → b, three edges, though s–b exists. Its main uses are mazes, cycle detection, connected components, and topological sorting of DAGs.
14.3.6DFS code: recursive and with an explicit stack
The shortest way to write DFS is recursively.
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);
}The method marks and prints v, then calls itself on each unvisited neighbor in turn. The loop waits for each call to return, so everything below the first neighbor is finished before the second: “deepest first.” When the neighbors run out, the method returns and the caller resumes; that return is backtracking.
Recursion keeps the stack for us as the call stack: the active dfs frames, in order, are the path from the start to the current vertex. An explicit stack works too:
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 up
else { visited[next] = true;
stack.push(next); }
}
}This imitates the recursion step by step. The top (peek) is the current vertex; each round seeks its first unvisited neighbor. Found, it is marked and pushed, like a recursive call; not found (next == null), the top is popped, like a return. The break makes the search descend at the first suitable neighbor.
An explicit stack also avoids StackOverflowError: Java's call stack is limited, and recursive DFS on a path of hundreds of thousands of vertices can overflow it, while an explicit stack lives on the heap.
Cost. Recursive DFS calls dfs once per vertex and walks each list once: O(n + m). The explicit-stack version rescans a list from the start at every peek, skipping marked neighbors again each time a vertex resurfaces, at most its degree plus one times; the worst case approaches Σ deg(v)2. A per-list “where I left off” index restores O(n + m). The code favors simplicity; for small degrees the difference is negligible.
14.3.7Code trace: recursive dfs(A)
We trace dfs(A, visited) on the graph of Figure 14.19, neighbors again in alphabetical order.
| Step | Event | Call stack | Printed (so far) |
|---|---|---|---|
| 1 | dfs(A): A is marked | A | A |
| 2 | A's neighbor B is new → dfs(B) | A B | A B |
| 3 | B: A marked, skip; C new → dfs(C) | A B C | A B C |
| 4 | C: skip B; D new → dfs(D) | A B C D | A B C D |
| 5 | D: skip C; G new → dfs(G) | A B C D G | A B C D G |
| 6 | G: skip D; E new → dfs(E) | A B C D G E | A B C D G E |
| 7 | E: B, C, G marked → dfs(E) returns | A B C D G | — |
| 8 | G: F new → dfs(F) | A B C D G F | A B C D G E F |
| 9 | F: A, G marked → returns; G has no neighbors left → returns | A B C D | — |
| 10 | D: H new → dfs(H) | A B C D H | A B C D G E F H |
| 11 | H: skip D → returns; D done → returns | A B C | — |
| 12 | C: E marked → returns; B: E marked → returns | A | — |
| 13 | A: F marked, skip; I new → dfs(I) | A I | A B C D G E F H I |
| 14 | I: skip A → returns; A done → returns | (empty) | — |
Steps 1–6 only go forward, the stack growing a frame each step: A → B → C → D → G → E. At E every neighbor is marked, and the first backtrack returns to G, which resumes its list: D and E are done, F is next.
A caller resumes where it left off, not from the start. In step 12 the search returns to C, whose list B, D, E has only E left, already marked below G. Each list is walked exactly once.
dfs(A), with the call stack on the right. (a) The deepest point: stack A B C D G E. (b) E has returned, and the search has descended to F below G. (c) G is finished, and the search has descended to H below D. (d) The last step: dfs(I) has returned and dfs(A) is about to finish; every vertex is visited.The order is A B C D G E F H I; the DFS tree edges are A–B, B–C, C–D, D–G, G–E, G–F, D–H, A–I; the stack peaks at six frames. The DFS tree is longer and thinner than the BFS tree: five edges from root to E, against two.
14.3.8Code trace: dfsStack(A)
Now the explicit-stack version: each round peeks, seeks the first unvisited neighbor, and pushes or pops.
| Round | peek | First unvisited neighbor | Operation | Stack (end of round) |
|---|---|---|---|---|
| start | — | — | push A | A |
| 1 | A | B | push B | A B |
| 2 | B | C | push C | A B C |
| 3 | C | D | push D | A B C D |
| 4 | D | G | push G | A B C D G |
| 5 | G | E | push E | A B C D G E |
| 6 | E | none | pop | A B C D G |
| 7 | G | F | push F | A B C D G F |
| 8 | F | none | pop | A B C D G |
| 9 | G | none | pop | A B C D |
| 10 | D | H | push H | A B C D H |
| 11 | H | none | pop | A B C D |
| 12 | D | none | pop | A B C |
| 13 | C | none | pop | A B |
| 14 | B | none | pop | A |
| 15 | A | I | push I | A I |
| 16 | I | none | pop | A |
| 17 | A | none | pop | (empty) |
The loop runs 17 rounds: 8 pushes (every vertex but A) and 9 pops (every vertex). Pushes visit vertices in the order A B C D G E F H I, as in the recursive version, and the stack always matches the call stack of Table 14.11. One algorithm; only the keeper of the stack differs.
Getting explicit-stack DFS by swapping BFS's queue for a stack: pop a vertex, push all its neighbors. Every vertex is still visited once, but not in DFS order: neighbors come out reversed, and siblings are marked together before the search goes deeper. Hence peek and “first neighbor, then break”.
14.3.9BFS versus DFS
| BFS | DFS | |
|---|---|---|
| Helper structure | queue (FIFO) | stack (LIFO) or recursion |
| Time (adjacency list) | O(n + m) | O(n + m) |
| Extra memory | O(n): widest level in the queue | O(n): longest path on the stack |
| Gives shortest paths? | yes (unweighted graphs) | no |
| Typical use | fewest steps, finding levels | cycle detection, components, mazes, topological sort |
Both take O(n + m): each vertex is processed once and each adjacency entry scanned once. An undirected graph has 2m entries, but the constant vanishes in big-O.
Both need O(n) extra memory at worst, in different worst cases: BFS suffers on wide graphs, where many neighbors enter the queue at once; DFS on deep ones, where the stack grows as long as the longest chain.
The traces assume alphabetical lists. Other list orders change the visiting order but not the visited set or the cost: from H, BFS gives H D C G B E F A I and DFS H D C B A F G E I. And replacing the stack of explicit-stack DFS with a queue, adding all neighbors at once, turns it into BFS: the searches differ in which end of the helper structure they read.
14.3.10Connected components
A traversal visits only its start's component. To find them all, restart from each unassigned vertex; each restart is a new component.
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);
}comp holds component numbers, −1 meaning “unassigned,” and doubles as the visited mark. The outer loop scans vertices 0 to n − 1, calling dfs(s, count++, comp) at each −1. The post-increment passes the old value, so the first component is 0. dfs writes the number into every vertex it reaches.
components. The three components are shown in three colors: comp = [0, 0, 0, 1, 1, 2].s | comp[s] | Action | count | comp (after) |
|---|---|---|---|---|
| — | — | Arrays.fill | 0 | [−1, −1, −1, −1, −1, −1] |
| 0 | −1 | dfs(0, 0): colors 0, 1, 2 | 1 | [0, 0, 0, −1, −1, −1] |
| 1 | 0 | skip | 1 | same |
| 2 | 0 | skip | 1 | same |
| 3 | −1 | dfs(3, 1): colors 3, 4 | 2 | [0, 0, 0, 1, 1, −1] |
| 4 | 1 | skip | 2 | same |
| 5 | −1 | dfs(5, 2): 5 alone | 3 | [0, 0, 0, 1, 1, 2] |
Inside dfs(0, 0): 0 is colored, then its neighbor 1, whose only neighbor 0 is done, so the call returns; then 2, likewise. The method returns 3: components {0, 1, 2}, {3, 4}, {5}.
The cost is O(n + m): n outer rounds, each vertex colored once with its list walked once, each edge seen once from each end. Afterward, “same component?” is the O(1) test comp[u] == comp[v], instead of an O(n + m) traversal per query.
14.3.11Testing whether a graph is bipartite
A graph is bipartite if its vertices can be split into two groups so that every edge runs between the groups; no edge joins two vertices of the same group.
Matching problems produce bipartite graphs: students on one side, projects on the other, an edge meaning “this student wants this project.” Two-shift scheduling and conflict-free grouping look the same.
Think of the groups as two colors, every neighbor of a vertex having the other color. BFS tests this: color the start 0 and each newly discovered vertex the opposite of its discoverer. A colored neighbor with the current vertex's color means the graph is not bipartite.
// 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;color starts at −1 (“uncolored”); 1 - color[w] flips 0 and 1. It is BFS plus a color check at colored neighbors.
Left graph. 0 gets color 0; its neighbors 1 and 5 get 1. Processing 1 colors 2 and 4 with 0; processing 5 finds 4 and 0 opposite, as they should be; processing 2 colors 3 with 1, and no later check conflicts. So color = [0, 1, 0, 1, 0, 1]: evens in one group, odds in the other. Every cycle is even: 0–1–2–3–4–5–0 has six edges, 0–1–4–5–0 and 1–2–3–4–1 four.
Triangle. 0 gets 0; 1 and 2 get 1. Processing 1 finds neighbor 2 with its own color, and the method returns false: three mutually adjacent vertices cannot share two colors.
The general rule: a graph is bipartite if and only if it has no odd-length cycle. Colors alternate around a cycle, and returning to the start with its original color requires an even number of edges; so C5 is not bipartite and C6 is. The test costs O(n + m), like BFS; a disconnected graph is handled by restarting at each uncolored vertex.
14.3.12Topological sorting and Kahn's algorithm
A student must take courses in an order where each follows all its prerequisites. With an arrow u → v when u is a prerequisite of v, this is a directed graph, and the order is a topological order.
A topological sort of a directed graph arranges its vertices so that for every arrow u → v, u comes first. A directed graph with no directed cycle is a directed acyclic graph (DAG). Only DAGs have topological orders.
Why cycles rule it out: given a → b → c → a, a must precede b, b precede c, and c precede a, so a precedes itself. Each course on the cycle waits for another, and none can start.
The order need not be unique: courses ready at the same time can go in any order, so the DAG 0 → 1, 0 → 2, 1 → 3, 2 → 3 allows 0 1 2 3 and 0 2 1 3. Uses include course plans, task dependencies, build order, and workflows.
Kahn's algorithm: a vertex of in-degree 0 has no prerequisites and can go first. Once output, its arrows count as deleted, lowering their heads' in-degrees; vertices reaching 0 become ready and wait in a queue.
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 resultList<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
}The Java code has three phases:
- Counting. Nested loops increment
indegat each arrow's head, in O(n + m). - Initialization. Vertices of in-degree 0 are enqueued.
- Main loop. Dequeue u, append it to the result, and run
--indeg[v]for each arrow's head; the prefix decrement yields the new value, and at 0, v is enqueued.
If the result has n vertices when the queue empties, it is a topological order; fewer means a directed cycle, whose vertices each wait for their predecessor, never reach in-degree 0, and are never enqueued.
14.3.13Code trace: kahn()
Six courses have the arrows 0 → 3, 1 → 3, 1 → 4, 2 → 0, 2 → 4, 3 → 5, 4 → 5; the lists are 0: [3] · 1: [3, 4] · 2: [0, 4] · 3: [5] · 4: [5] · 5: [].
indeg = [1, 0, 0, 2, 2, 2]. Courses 1 and 2, which have no prerequisites, are in the queue.Counting. 0's list sets indeg[3] to 1; 1's raises indeg[3] to 2 and indeg[4] to 1; 2's raises indeg[0] to 1 and indeg[4] to 2; those of 3 and 4 raise indeg[5] to 2. The result is [1, 0, 0, 2, 2, 2], and 1 and 2 enter the queue.
Removed u | Deleted arrows and new in-degrees | Enqueued | indeg (after) | Queue | result |
|---|---|---|---|---|---|
| — | — | 1, 2 | [1, 0, 0, 2, 2, 2] | [1, 2] | [] |
| 1 | 1 → 3: indeg[3] = 1; 1 → 4: indeg[4] = 1 | — | [1, 0, 0, 1, 1, 2] | [2] | [1] |
| 2 | 2 → 0: indeg[0] = 0; 2 → 4: indeg[4] = 0 | 0, 4 | [0, 0, 0, 1, 0, 2] | [0, 4] | [1, 2] |
| 0 | 0 → 3: indeg[3] = 0 | 3 | [0, 0, 0, 0, 0, 2] | [4, 3] | [1, 2, 0] |
| 4 | 4 → 5: indeg[5] = 1 | — | [0, 0, 0, 0, 0, 1] | [3] | [1, 2, 0, 4] |
| 3 | 3 → 5: indeg[5] = 0 | 5 | [0, 0, 0, 0, 0, 0] | [5] | [1, 2, 0, 4, 3] |
| 5 | (empty list) | — | [0, 0, 0, 0, 0, 0] | [] | [1, 2, 0, 4, 3, 5] |
Removing 1 lowers 3 and 4 but not to 0: 3 waits for 0, 4 for 2. Removing 2 readies 0 and 4; removing 0 readies 3. indeg[5] drops to 1 when 4 leaves and to 0 when 3 does.
kahn(). (a) 2 has been dequeued and added to the result; its arrows are not yet processed, and the queue is empty. (b) 2 → 4 is deleted too; queue 0, 4. (c) 4 → 5 is deleted; 5 still waits for 3. (d) The end: result 1 2 0 4 3 5.The result 1 2 0 4 3 5 has size 6 = n: no cycle, and every arrow's source comes first (for 2 → 0, 2 is second, 0 third). One enqueue per vertex, one decrement per arrow: O(n + m).
With arrows 0 → 1, 1 → 2, 2 → 0, 2 → 3, the in-degrees are [1, 1, 1, 1]; the queue starts empty and the loop never runs. The empty result, 0 < 4, reveals a cycle. Vertex 3 is off the cycle but depends on 2, so it cannot be ordered either.
DFS also sorts topologically: a vertex's call finishes after those of everything reachable from it, so reverse finishing order is a topological order, with cycles caught by the back-edge check of the next section.
Not checking Kahn's result. On a cyclic graph it returns a short list without error; unless its size is compared with n, the list passes for an order and the cycle's courses silently vanish from the plan.
14.3.14Detecting cycles in a directed graph
In an undirected graph, DFS meeting a marked neighbor other than its parent means a cycle. In a directed graph an arrow to a marked vertex is not enough: the vertex must lie on the current DFS path. Three colors tell the cases apart:
- white (0): not yet visited;
- gray (1): visited and on the current DFS path; its call has not finished;
- black (2): visited and finished; everything reachable from it has been examined.
// 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;
}A gray head means the DFS path leads from it to the current vertex, and the arrow closes a cycle: a back edge. A black head has no path back, or the current vertex would have been visited during its call, so it closes no cycle.
| Step | Event | color (0…4) |
|---|---|---|
| 1 | 0 turns gray | [1, 0, 0, 0, 0] |
| 2 | 0 → 1: 1 white, call; 1 turns gray | [1, 1, 0, 0, 0] |
| 3 | 1 → 2: 2 white, call; 2 turns gray | [1, 1, 1, 0, 0] |
| 4 | 2 has no arrows: 2 black, false | [1, 1, 2, 0, 0] |
| 5 | 1's arrows done: 1 black, false | [1, 2, 2, 0, 0] |
| 6 | 0 → 3: 3 white, call; 3 turns gray | [1, 2, 2, 1, 0] |
| 7 | 3 → 2: 2 black, no cycle, continue | [1, 2, 2, 1, 0] |
| 8 | 3 → 4: 4 white, call; 4 turns gray | [1, 2, 2, 1, 1] |
| 9 | 4 → 0: 0 gray → back edge, true | [1, 2, 2, 1, 1] |
| 10 | true is passed back to 3, then to 0 | [1, 2, 2, 1, 1] |
Step 7 shows the subtlety: 3 → 2 leads to a visited vertex, and asking only “visited?” would report a false cycle, but 2 is black, with no path back to 3. In step 9, 0 is gray: 0 → 3 → 4 is still on the call stack, and 4 → 0 closes the cycle 0 → 3 → 4 → 0. true returns at once through every level.
Forgetting color[v] = 2, so finished vertices stay gray. In the DAG 0 → 1, 0 → 2, 1 → 2, vertices 0, 1, 2 turn gray, and 2 and 1 return still gray; back at 0, the arrow 0 → 2 finds color[2] == 1 and reports a nonexistent cycle. Directed graphs need gray and black, not just visited and unvisited.
The cost is that of DFS: each vertex turns gray and black once, each arrow is examined once. If some vertices are unreachable from 0, hasCycle restarts at each white one, still O(n + m).
14.4Shortest paths
A map app's route, a router's fastest packet path, a game character's way to its goal: one graph problem. Without weights, length is the edge count and BFS solves it; with weights, length is the weight sum and Dijkstra's algorithm solves it.
14.4.1The unweighted shortest-path problem
The single-source shortest-path problem gives a source vertex s and asks for the length of the shortest path from s to every vertex, called that vertex's distance from s. A vertex unreachable from s has distance ∞.
Without weights, each edge counts 1, and BFS solves the problem: a vertex on level k has distance exactly k.
In an unweighted graph, BFS from s discovers the vertices in order of their distance from s. When a vertex is first reached from a vertex w, its distance is distance(w) + 1.
The queue always holds distance-k vertices in front and distance-k + 1 vertices behind. Initially it holds only s, at 0; when w at distance k leaves, its undiscovered neighbors join the back with k + 1, keeping the pattern. Take u at distance k + 1: a path of k + 1 edges reaches it, its next-to-last vertex w' at distance k. No vertex closer than k is adjacent to u, or u would be closer, and all distance-k vertices leave before any at k + 1. So u is discovered from distance k and gets k + 1. □
Two three-edge paths reach 7: 1 → 4 → 6 → 7 and 1 → 3 → 8 → 7. BFS finds one, depending on list order, but the length is 3 either way: shortest paths need not be unique, shortest lengths are.
14.4.2Algorithm and code
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 distThis is BFS with a distance array as the visited array, “distance = ∞” meaning “unvisited.” A newly discovered vertex gets its discoverer's distance plus one, which by Theorem 14.2 is final.
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;
}Here the graph is a Map<Integer, List<Integer>> named graph, from vertex to neighbor list; vertices start at 1, hence a map rather than an array.
INFstands for ∞ asInteger.MAX_VALUE.- Every distance starts at
INF; the source gets 0 and is enqueued (offerappends, likeadd). - The main loop is BFS: each neighbor still at
INFgetsdist.get(w) + 1and is enqueued.
The cost is BFS's, O(n + m), with HashMap operations O(1) on average.
Adding 1 to ∞. Integer.MAX_VALUE + 1 overflows to −2,147,483,648. In the code, the w that gets 1 added has left the queue, so its distance is finite. Weighted graphs are riskier, which is why Dijkstra's Java version uses long distances and relaxes only from settled vertices.
14.4.3Code trace: shortestPath(1)
Let the source be 1 in the graph of Figure 14.28, with neighbor lists in increasing order: 1: [2, 3, 4] · 2: [1, 5] · 3: [1, 8] · 4: [1, 6] · 5: [2, 8] · 6: [4, 7] · 7: [6, 8] · 8: [3, 5, 7].
Removed w | Neighbors | Updated | dist (1…8) | Queue |
|---|---|---|---|---|
| — | — | dist[1] = 0 | 0 ∞ ∞ ∞ ∞ ∞ ∞ ∞ | [1] |
| 1 | 2, 3, 4 | 2, 3, 4 ← 0 + 1 = 1 | 0 1 1 1 ∞ ∞ ∞ ∞ | [2, 3, 4] |
| 2 | 1, 5 | 5 ← 1 + 1 = 2 | 0 1 1 1 2 ∞ ∞ ∞ | [3, 4, 5] |
| 3 | 1, 8 | 8 ← 1 + 1 = 2 | 0 1 1 1 2 ∞ ∞ 2 | [4, 5, 8] |
| 4 | 1, 6 | 6 ← 1 + 1 = 2 | 0 1 1 1 2 2 ∞ 2 | [5, 8, 6] |
| 5 | 2, 8 | — (both known) | 0 1 1 1 2 2 ∞ 2 | [8, 6] |
| 8 | 3, 5, 7 | 7 ← 2 + 1 = 3 | 0 1 1 1 2 2 3 2 | [6, 7] |
| 6 | 4, 7 | — (both known) | 0 1 1 1 2 2 3 2 | [7] |
| 7 | 6, 8 | — | 0 1 1 1 2 2 3 2 | [] |
Three points stand out. When 2 leaves, neighbor 1 keeps distance 0: in BFS the first distance found is shortest. When 5 leaves, neighbor 8 already has 2; via 5 it would be 2 + 1 = 3, and the code does not even look, since 8 is no longer INF.
And 8, enqueued before 6, writes 7's distance, so the path found is 1 → 3 → 8 → 7; another list order might give 1 → 4 → 6 → 7, still 3. The final row matches Figure 14.28.
shortestPath(1). (a) 1 processed; 2, 3, 4 have distance 1, queue 2, 3, 4. (b) 8 processed; 7 gets distance 3, queue 6, 7.The code returns distances, not paths; for paths, store each vertex's discoverer in a prev array. Dijkstra does the same, and 14.4.8 traces rebuilding a path backward from the target.
14.4.4The weighted shortest-path problem
With weights the problem is the same, but length is the sum of edge weights: distances, times, or costs.
In Figure 14.30 BFS finds A → B → D → F, the fewest edges (three), but it weighs 4 + 10 + 11 = 25, while the four-edge A → C → E → D → F weighs 20. Fewest edges is not least weight; BFS is not enough.
The solution is the algorithm Edsger W. Dijkstra devised in 1956 and published in 1959. It has one requirement: edge weights must not be negative, for reasons that emerge in its correctness proof (14.4.9).
14.4.5Dijkstra's algorithm: idea and pseudocode
Dijkstra's algorithm keeps a tentative distance for each vertex, the length of the shortest path found so far: 0 for the source, ∞ for the rest. Each step:
- picks, among vertices not yet settled, the one with the smallest tentative distance, whose distance is now final;
- updates its neighbors' distances by relaxation.
To relax an edge (u, v) is to test whether going through u beats the known path to v: compute alt = dist[u] + weight(u, v), and if alt < dist[v], set dist[v] = alt and prev[v] = u.
The name pictures an upper bound loosening like a spring. A tentative distance never drops below the true distance; each relaxation either leaves it alone or moves it closer.
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 QQ is a priority queue of unsettled vertices keyed by tentative distance; Q.removeMin() returns the closest. When relaxation lowers a distance, the vertex's priority must drop too. prev[] records each vertex's predecessor on its shortest path, and paths are read backward from it.
Prioritizing by vertex number. The queue must return the vertex with the smallest distance, not the smallest number; that is why the comparator of the Java PriorityQueue<long[]> compares the entry's second element, the distance.
14.4.6Dijkstra 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, distance}
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;
}- Input.
adj.get(u)lists u's neighbors asint[]{v, w}pairs, neighbor and weight. The caller allocatesprev;dijkstrafills it. - Setup. The
longarraydis filled withLong.MAX_VALUE, standing for ∞;d[s] = 0;previs filled with −1, “no predecessor.” - Queue. Entries are
{vertex, distance}pairs; the comparatorLong.compare(a[1], b[1])orders by distance, sopollreturns the closest. - Stale check.
if (e[1] > d[u]) continue;skips an entry whose distance exceeds the vertex's current one. - Relaxation. For each neighbor,
alt = d[u] + k[1]; ifalt < d[v], the distance andprevare updated and a new{v, alt}entry is added.
Java's PriorityQueue has no decrease-key, so the code adds a new entry for v and leaves the old one. When the old entry surfaces, its distance exceeds the since-lowered d[v]: a stale entry, skipped by continue. This is lazy deletion: ignore old entries when they surface instead of removing them.
Taking the stale-entry line for a correctness requirement, or deleting it as useless. With non-negative weights, distances come out right without it: by then d[u] is smaller and the neighbors were relaxed with it, so alt < d[v] fails. The line saves rescanning stale entries' neighbors, which costs real time on dense graphs.
14.4.7Code trace: dijkstra(adj, A)
Give the nine-vertex traversal graph weights: A–B: 4, A–F: 7, A–I: 3, B–C: 1, B–E: 2, C–D: 7, C–E: 3, D–G: 3, D–H: 5, E–G: 2, F–G: 2. The graph is undirected, each edge in both lists, and the source is A.
d[A] = 0, all other distances ∞, prev empty, and only {A, 0} in the queue.Each row below is one poll. The queue column shows the entries left after that row's relaxations, sorted by distance.
| Removed entry | Status | Relaxations | Queue (after) |
|---|---|---|---|
| {A, 0} | A settled (0) | B: 0 + 4 = 4 < ∞ ✓; F: 0 + 7 = 7 ✓; I: 0 + 3 = 3 ✓ | {I,3} {B,4} {F,7} |
| {I, 3} | I settled (3) | A: 6 ≥ 0 | {B,4} {F,7} |
| {B, 4} | B settled (4) | A: 8 ≥ 0; C: 4 + 1 = 5 ✓; E: 4 + 2 = 6 ✓ | {C,5} {E,6} {F,7} |
| {C, 5} | C settled (5) | B: 6 ≥ 4; D: 5 + 7 = 12 ✓; E: 8 ≥ 6 | {E,6} {F,7} {D,12} |
| {E, 6} | E settled (6) | B: 8 ≥ 4; C: 9 ≥ 5; G: 6 + 2 = 8 ✓ | {F,7} {G,8} {D,12} |
| {F, 7} | F settled (7) | A: 14 ≥ 0; G: 9 ≥ 8 | {G,8} {D,12} |
| {G, 8} | G settled (8) | D: 8 + 3 = 11 < 12 ✓; E: 10 ≥ 6; F: 10 ≥ 7 | {D,11} {D,12} |
| {D, 11} | D settled (11) | C: 18 ≥ 5; G: 14 ≥ 8; H: 11 + 5 = 16 ✓ | {D,12} {H,16} |
| {D, 12} | stale: 12 > d[D] = 11 | skipped | {H,16} |
| {H, 16} | H settled (16) | D: 21 ≥ 11 | (empty) |
The critical row is G's. D stands at 12, via C (A → B → C → D: 4 + 1 + 7). Settling G relaxes D–G to 8 + 3 = 11 < 12: D drops to 11, prev[D] = G, and {D, 11} joins the queue beside the old {D, 12}. {D, 11} surfaces first and settles D; {D, 12} follows, stale since 12 > 11, and is skipped.
Vertices settle in distance order, A (0), I (3), B (4), C (5), E (6), F (7), G (8), D (11), H (16), the property behind Dijkstra's correctness. There were 10 polls, 10 insertions (1 initial, 9 successful relaxations), and 22 relaxation attempts, 2m = 22.
d[D] = 12 (through C). (b) A shorter path through G is found: d[D] = 11, prev[D] = G; the old {D, 12} entry is stale. (c) The stale entry comes out and is skipped. (d) Final state: the bold edges form the shortest-path tree.| A | B | C | D | E | F | G | H | I | |
|---|---|---|---|---|---|---|---|---|---|
d | 0 | 4 | 5 | 11 | 6 | 7 | 8 | 16 | 3 |
prev | – | A | B | G | B | A | E | D | A |
prev holds the shortest-path tree: from H, H ← D ← G ← E ← B ← A, so A → B → E → G → D → H, five edges of length 4 + 2 + 2 + 3 + 5 = 16. The four-edge A → F → G → D → H has length 7 + 2 + 3 + 5 = 17.
14.4.8Reconstructing the path
To get the path from s to t, follow prev from t until −1. The chain runs backward, so each vertex goes to the front of the list.
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
}Starting at v = t, each round prepends v and steps back with v = prev[v], stopping after the source, whose prev is −1. LinkedList.addFirst is constant time, so the cost is O(path length), at most O(n).
The final check catches unreachable targets: prev[t] = −1 stops the chain at once, leaving t first, and a first item other than s yields an empty list.
Run from A on the graph of Figure 14.30, Dijkstra leaves prev as B ← A, C ← A, D ← E, E ← C, F ← D. The call path(prev, A, F) goes:
v = F: list [F];v = prev[F] = D.v = D: list [D, F];v = prev[D] = E.v = E: list [E, D, F];v = prev[E] = C.v = C: list [C, E, D, F];v = prev[C] = A.v = A: list [A, C, E, D, F];v = prev[A] = −1, and the loop ends.
Five rounds, five vertices. The first is A, so F is reachable: A → C → E → D → F, of length d[F] = 2 + 3 + 4 + 11 = 20.
prev chain has been followed from F to A, each vertex added to the front of the list: p = [A, C, E, D, F].Reusing prev for another source. It is the shortest-path tree of the source Dijkstra ran from: prev = [−1, 0, 0, 1, 3], built from 0, gives 0 → 1 → 3 → 4. Asked for 2 to 4, the chain 4, 3, 1, 0 never meets 2, and the result is empty; run Dijkstra from 2 instead.
14.4.9Dijkstra: analysis and correctness
Time. Two implementations are common:
| Implementation | Complexity | Why? |
|---|---|---|
| Priority queue (binary heap) | O((n + m) log n) | n removals of the minimum, at most m insertions or updates, each O(log n) |
| No queue (array scan) | O(n2 + m) = O(n2) | Each step scans n vertices for the closest; neighbor checks total m |
With a heap, each successful relaxation adds an entry, at most m + 1 in all, each insertion or removal O(log m). As m ≤ n2, log m ≤ 2 log n, giving O((n + m) log n).
Sparse graphs (m ≈ n) favor the heap, O(n log n) against the array scan's O(n2); on very dense ones (m ≈ n2) the heap rises to O(n2 log n) and the O(n2) scan can win.
If no edge weight is negative, then when Dijkstra's algorithm settles a vertex, its tentative distance equals its true distance from the source.
Let u be the unsettled vertex of smallest tentative distance, and take any path from s to u. It leaves the settled region somewhere first; let y be the first vertex outside and x its predecessor. (x, y) was relaxed when x settled, so dist[y] is at most the length of the path up to y, and dist[u] ≤ dist[y] by the choice of u. The rest of the path has non-negative edges, length ≥ 0. So the path is at least dist[y] ≥ dist[u] long: none is shorter than dist[u]. □
Non-negativity is used once: “length ≥ 0.” A negative edge could later shorten a settled vertex's distance, and the pseudocode then errs. The stale-entry Java version may recover by reprocessing vertices, but loses its time bound, and a negative cycle makes it run forever. Negative weights call for the Bellman-Ford algorithm.
Take arrows s → a: 2, s → b: 3, b → a: −2, a → c: 1. Dijkstra settles s (0), then a (2), giving c distance 3, then b (3). Via b → a, 3 + (−2) = 1 is a's true distance, but a is already settled at 2, and the pseudocode never revisits it, so c stays at 3 instead of 1 + 1 = 2. Here the Java version re-adds a with 1 and processes it again, correcting c to 2: right answer, but a vertex processed twice.
14.5Minimum spanning trees
A municipality wants water in every village; pipe between any two villages has a cost, and water need only reach each village by some route. Which network costs least? This minimum spanning tree problem also arises in power and communication networks, clustering, and inside other algorithms.
14.5.1Spanning trees and the MST
A spanning tree of a connected graph is a connected, acyclic subgraph containing all its vertices; with n vertices it has exactly n − 1 edges. A minimum spanning tree (MST) of a weighted graph is a spanning tree of smallest total edge weight.
Why no cycles? With positive weights, dropping a cycle edge lowers the total while the rest of the cycle keeps its endpoints connected, so the cheapest connected subgraph is a tree. The n − 1 is the lower bound of 14.1.7, met exactly by acyclic connected graphs.
The edges weigh 62 in all; the MST takes nine of them, weighing 14. Spanning trees are far too many to try, so the classic algorithms, Kruskal's and Prim's, are greedy: each step takes the best-looking choice and never revisits it. Greed works here thanks to this property.
Split the vertices of a graph into two nonempty groups. Among the edges joining the two groups, one of smallest weight belongs to some minimum spanning tree.
Let e = (u, v) be the lightest crossing edge and T an MST without it. T has a path from u to v, which crosses between the groups along some edge f. Adding e and removing f leaves a spanning tree: the one cycle e creates is broken by removing f. As weight(e) ≤ weight(f), the new tree weighs no more than T, so it is an MST containing e. □
Kruskal and Prim apply this theorem in two ways. In Prim, one group is the tree built so far; in Kruskal, the groups are the trees containing the two endpoints of the chosen edge.
14.5.2Disjoint sets (Union-Find)
Kruskal tries edges from lightest to heaviest, asking of each whether it closes a cycle, that is, whether chosen edges already connect its endpoints. Disjoint sets (union-find) answer this quickly, managing non-overlapping sets with two operations:
find(x)returns the representative of x's set; two items are in the same set exactly whenfindgives them the same representative.union(u, v)merges the sets of u and v and returnstrueif they differ; if they are already the same set, it does nothing and returnsfalse.
The slides use only this behavior; the class below is an implementation that reproduces the parent arrows of their trace.
class UnionFind {
private final int[] parent, size;
UnionFind(int n) { // each vertex its own set
parent = new int[n]; size = new int[n];
for (int i = 0; i < n; i++) { parent[i] = i; size[i] = 1; }
}
int find(int x) { // find the root, compress the path
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
boolean union(int u, int v) {
int a = find(u), b = find(v);
if (a == b) return false; // same set: would close a cycle
if (size[a] > size[b]) { int t = a; a = b; b = t; }
parent[a] = b; size[b] += size[a]; // smaller tree under the larger
return true;
}
}Each set is a tree: parent[x] is x's parent, and the root, its own parent, represents the set. Initially there are n one-vertex trees. Two refinements give speed:
- Union by size. The smaller tree's root goes under the larger's, keeping heights at most log n; on a tie, u's root goes under v's.
- Path compression.
findlinks every vertex on its way directly to the root, so repeat queries take one step.
Together they make each operation cost O(α(n)) over a sequence, where α, the inverse Ackermann function, stays ≤ 4 even for n the number of atoms in the universe: in practice, constant time.
14.5.3Kruskal'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)Kruskal's rule: pick the lightest edge that closes no cycle. Starting from n one-vertex sets, it tries edges by increasing weight; an edge joining different sets enters T and merges them, one inside a set is skipped. After n − 1 edges the tree is done.
Midway, the chosen edges form a forest whose trees merge into one. A disconnected graph never reaches n − 1; every edge is tried, giving a minimum spanning forest, one tree per component.
| Phase | Cost |
|---|---|
| Sorting the edges | O(m log m) |
2m find + (n − 1) union | O(m · α(n)) |
| Total | O(m log n) |
Sorting dominates: as log m ≤ 2 log n, O(m log m) = O(m log n), and the nearly linear Union-Find work changes nothing. A DFS per edge for cycle checks would cost O(m · n).
14.5.4Kruskal step by step
We run Kruskal on the graph of Figure 14.34, trying equal-weight edges in the order of its caption.
| Edge | Weight | Decision | Sets merged / reason | |T| | Total |
|---|---|---|---|---|---|
| I–J | 0 | accept | {I} + {J} | 1 | 0 |
| A–E | 1 | accept | {A} + {E} | 2 | 1 |
| C–I | 1 | accept | {C} + {I, J} | 3 | 2 |
| E–F | 1 | accept | {A, E} + {F} | 4 | 3 |
| G–H | 1 | accept | {G} + {H} | 5 | 4 |
| B–D | 2 | accept | {B} + {D} | 6 | 6 |
| C–J | 2 | reject | C and J already in {C, I, J} | 6 | 6 |
| D–E | 2 | accept | {B, D} + {A, E, F} | 7 | 8 |
| D–H | 2 | accept | {A, B, D, E, F} + {G, H} | 8 | 10 |
| A–D | 4 | reject | A and D in the same set | 8 | 10 |
| B–C | 4 | accept | large set + {C, I, J} | 9 | 14 |
C–J is rejected because C–I and I–J already connect C and J; it would close C–I–J–C. Likewise A–D, with A–E–D in place. Accepting B–C makes |T| = 9 = n − 1, total 0 + 1 + 1 + 1 + 1 + 2 + 2 + 2 + 4 = 14, and the other seven edges (C–H, G–I, A–B, D–F, H–I, F–G, D–G) are never examined.
Judging cycles by whether the endpoints already touch chosen edges. D and E both do (B–D, A–E), yet lie in different trees, so D–E is accepted. What counts is being in the same set.
14.5.5Code trace: kruskal() and 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;
}Edges are {u, v, w} arrays, sorted by weight; Java's object sort is stable, so ties keep input order. uf.union(e[0], e[1]) checks and merges in one call, a true adding the edge; break ends the loop at |T| = n − 1.
Inside Union-Find, with A = 0, …, J = 9, the “linked” column shows which root union attaches to which.
| Edge | find roots | union | Linked | Parents changed |
|---|---|---|---|---|
| I–J (0) | I, J | true | I → J (equal sizes) | I → J |
| A–E (1) | A, E | true | A → E (equal) | A → E |
| C–I (1) | C, J | true | C → J (1 < 2) | C → J |
| E–F (1) | E, F | true | F → E (1 < 2) | F → E |
| G–H (1) | G, H | true | G → H (equal) | G → H |
| B–D (2) | B, D | true | B → D (equal) | B → D |
| C–J (2) | J, J | false | — | — |
| D–E (2) | D, E | true | D → E (2 < 3) | D → E |
| D–H (2) | E, H | true | H → E (2 < 5) | H → E |
| A–D (4) | E, E | false | — | — |
| B–C (4) | E, J | true | J → E (3 < 7) | B → E (path compression), J → E |
For C–J, find(C) follows C → J and find(J) returns J: same root, false, skipped. In the last step find(B) follows B → D → E, path compression links B to E, and J's tree (3 vertices) goes under E's (7). The single root E is reached by I and C through J, by G through H.
Nine edges of weight 14 are returned; eleven of eighteen edges were tried, but sorting, the dominant cost, covers all eighteen.
14.5.6A second example: airports
Goodrich, Tamassia, and Goldwasser give nine U.S. airports and 19 routes, weighted in miles; we want the shortest network connecting them all.
| Route (miles) | Decision | Reason |
|---|---|---|
| JFK–PVD 144, JFK–BWI 184, JFK–BOS 187 | accept | four airports join around JFK |
| SFO–LAX 337 | accept | a separate tree in the west |
| ORD–BWI 621 | accept | ORD joins the eastern tree |
| ORD–JFK 740 | reject | ORD and JFK already connected via BWI (cycle) |
| ORD–DFW 802 | accept | DFW joins the eastern tree |
| ORD–PVD 849, ORD–BOS 867 | reject | both ends in the eastern tree |
| BWI–MIA 946 | accept | MIA joins the eastern tree |
| JFK–MIA 1090, DFW–MIA 1121 | reject | both ends in the eastern tree |
| LAX–DFW 1235 | accept | western and eastern trees merge; 8 = n − 1 edges |
The total is 144 + 184 + 187 + 337 + 621 + 802 + 946 + 1235 = 4456 miles. Thirteen routes are tried; the six longest (1258, 1391, 1464, 1846, 2342, 2704) never are. Short routes group nearby airports, and long ones enter only as bridges between groups: LAX–DFW, the shortest bridge between west and east, must be in the MST by the cut property.
14.5.7Prim'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 TPrim grows one tree from a single vertex, adding each step the lightest edge to a vertex outside it. The cut property makes this safe: the tree must connect to the rest somehow, and this is the cheapest connection.
Like Dijkstra, Prim starts at a vertex and repeatedly takes a priority queue's smallest item, but the key differs: Dijkstra's is the total distance from the source, Prim's the weight of the single edge to the tree. Unlike Kruskal, there is always one tree, so “is the head in the tree?” detects cycles without Union-Find.
14.5.8Prim in Java
The Java version in the slides is the lazy version, built on a priority queue.
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: would close 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;
}Entries are {u, v, w}: tree endpoint, candidate, weight (the comparator's only concern). inTree marks members.
- The start
sjoins, and its edges are enqueued. - Until the queue empties or n − 1 edges are chosen, take the lightest entry.
- If candidate
e[1]is in the tree, the entry is stale (it would close a cycle), andcontinueskips it. - Otherwise the candidate joins, the edge enters T, and the new vertex's edges leaving the tree are enqueued.
“Lazy”: when v joins, old entries to it stay queued and are skipped on removal, as with Dijkstra's stale entries.
Cost. Each edge is enqueued at most once, from the endpoint that joins first; later, !inTree[f[0]] blocks it. At most m insertions and removals at O(log m) give O(m log n) time and O(m) queue memory. With a matrix and an array scan, Prim takes O(n2), better for very dense graphs.
Deleting the stale check (if (inTree[e[1]]) continue;). In Dijkstra it only saved time; in Prim it guarantees correctness. Without it, a leftover entry to v adds an edge between two tree vertices, creating a cycle.
14.5.9Prim step by step
Start Prim at A on Figure 14.34, breaking ties alphabetically by tree endpoint, then candidate.
| Step | Chosen edge | Joins tree | New candidates enqueued | Total |
|---|---|---|---|---|
| 0 | — | A | A–E 1, A–D 4, A–B 5 | 0 |
| 1 | A–E (1) | E | E–D 2, E–F 1 | 1 |
| 2 | E–F (1) | F | F–D 5, F–G 7 | 2 |
| 3 | E–D (2) | D | D–B 2, D–G 11, D–H 2 | 4 |
| 4 | D–B (2) | B | B–C 4 | 6 |
| 5 | D–H (2) | H | H–C 4, H–G 1, H–I 6 | 8 |
| 6 | H–G (1) | G | G–I 4 | 9 |
| 7 | B–C (4) (stale A–D 4 skipped first) | C | C–I 1, C–J 2 | 13 |
| 8 | C–I (1) | I | I–J 0 | 14 |
| 9 | I–J (0) | J | — | 14 |
When D joins in step 3, the entries A–D 4 and F–D 5 go stale but stay queued. In step 7, A–D leads the weight-4 entries alphabetically, is skipped as stale, and B–C is chosen. In step 9, J joins with n − 1 = 9 edges, leaving eight entries unexamined.
Prim's tree, A–E, E–F, E–D, D–B, D–H, H–G, B–C, C–I, I–J, has Kruskal's edges in another order: Kruskal began with I–J (weight 0), which Prim reaches last.
Alphabetical ties are a hand-tracing rule. The Java code takes tied entries in PriorityQueue's internal order, which guarantees nothing and depends on insertion order, hence on the lists. With alphabetical lists it picks G–I instead of B–C in step 7, then I–J and I–C: A–E, E–F, E–D, D–B, D–H, H–G, G–I, I–J, I–C. With edges listed in the order of the caption of Figure 14.34, it picks H–C in step 7. Each tree is an MST of weight 14: ties allow several MSTs, all of equal weight.
Start Prim at J on the same graph (ties alphabetical).
- J–I 0 and J–C 2 are enqueued; J–I brings in I, adding I–C 1, I–G 4, I–H 6.
- I–C 1 brings in C, adding C–B 4 and C–H 4.
- J–C 2 is now stale and skipped. Of C–B, C–H, and I–G at weight 4, C–B comes first, and B joins.
The first edges are J–I (0), I–C (1), C–B (4); then B–D, D–E, E–A, E–F, D–H, H–G, total 14. The start changes the building order, not the total.
14.5.10Kruskal versus Prim
| Kruskal | Prim | |
|---|---|---|
| Idea | tries edges lightest first, merging trees | grows one tree from a start vertex |
| Intermediate state | forest (several trees) | one tree |
| Data structures | sorted edge list and Union-Find | priority queue, mark 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(n2) with a matrix |
| When? | sparse graph, edges given as a list | adjacency list at hand; O(n2) version for very dense graphs |
Both are greedy, rest on the cut property, and find the same total (14 here), though ties may yield different trees. The data's form also matters: edges read as a list suit Kruskal; adjacency lists with a natural start vertex suit Prim.
Confusing the MST with the shortest-path tree. Dijkstra's prev tree minimizes path lengths from the source; the MST minimizes the edge sum. In the triangle A–B 2, B–C 2, A–C 3, the MST is A–B, B–C (total 4), where A to C has length 4, though the edge A–C has length 3.
14.6Summary
- A graph G = (V, E) has n vertices and m edges. Unlike a tree, it has no root, may have cycles, and may offer several routes to a vertex. Edges may be directed or not, weighted or not.
- A vertex's degree counts its incident edges. By the handshake lemma Σ deg(v) = 2m; directed in- and out-degrees each sum to m. Simple undirected graphs have m ≤ n(n − 1)/2, connected ones m ≥ n − 1.
- A path is a sequence of vertices joined by edges; a cycle ends where it starts. A graph with paths between all vertex pairs is connected; a largest mutually reachable group is a connected component.
- An adjacency matrix takes O(n2) memory, with O(1) edge queries and O(n) neighbor visits; an adjacency list takes O(n + m), with O(deg v) neighbor visits. Lists suit sparse graphs; matrices suit dense ones with frequent edge queries.
- BFS goes level by level with a queue, DFS deepest first with a stack or recursion, both O(n + m) with lists. Vertices are marked on entering the queue or stack; without marks, cycles trap the traversal.
- Three applications follow. Connected components: a new traversal from each uncolored vertex. Bipartiteness: 2-coloring by BFS, possible exactly without odd cycles. Directed cycle detection: three-color DFS, where an arrow to a gray vertex is a back edge.
- Topological orders exist only for DAGs. Kahn's algorithm dequeues in-degree-0 vertices and deletes their arrows, in O(n + m); fewer than n results reveal a cycle.
- BFS finds unweighted shortest paths, the first distance found being shortest. For non-negative weights, Dijkstra settles the closest unsettled vertex and relaxes its neighbors, in O((n + m) log n) with a heap or O(n2) by array scan; paths are read backward through
prev. - A minimum spanning tree connects all vertices with n − 1 edges of least total weight. Kruskal sorts edges and keeps those closing no cycle, via Union-Find; Prim grows one tree by its lightest outgoing edge. Both are O(m log n) and rest on the cut property.
| Algorithm | Helper structure | Time (with adjacency lists) | Produces |
|---|---|---|---|
| BFS | queue | O(n + m) | level order, unweighted shortest paths |
| DFS | stack / recursion | O(n + m) | depth-first traversal, cycles, components |
| Topological sort (Kahn) | 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 and Union-Find | O(m log n) | minimum spanning tree |
| Prim | priority queue | O(m log n) | minimum spanning tree |
The table shows the chapter's lesson: the data structure shapes the algorithm. One traversal skeleton becomes BFS with a queue, DFS with a stack, Dijkstra with a priority queue keyed by distance, and Prim with one keyed by edge weight.
14.7Key terms
The glossary gives each term's Turkish equivalent and its use in other chapters.
- graph
- vertex, node
- edge
- adjacent
- degree
- in-degree
- out-degree
- directed graph (digraph)
- weighted graph
- path
- cycle
- connected component
- complete graph
- edge list
- adjacency list
- adjacency matrix
- traversal
- breadth-first search (BFS)
- depth-first search (DFS)
- bipartite graph
- directed acyclic graph (DAG)
- topological sort
- relaxation
- priority queue
- spanning tree
- minimum spanning tree (MST)
- disjoint sets (union-find)
- path compression
- Kruskal's algorithm
- Prim's algorithm
14.8Test Yourself
What are the fewest and the most edges a simple, undirected, connected graph on 8 vertices can have?
Answer
At least n − 1 = 7 (the graph is then a tree), at most n(n − 1)/2 = 8 · 7 / 2 = 28 (the complete graph K8).
How many cells are 1 in the adjacency matrix of the directed graph with arrows 0 → 1, 0 → 2, 2 → 1, 3 → 0? How do you read 0's out-degree and 1's in-degree from the matrix?
Answer
Each arrow fills one cell: 4 cells. 0's out-degree is the sum of row 0,
m[0][1] + m[0][2] = 2; 1's in-degree is the sum of column 1,m[0][1] + m[2][1] = 2.On the nine-vertex traversal graph (neighbors alphabetical), what does
bfs(E)print? How many edges is A from E?Answer
E B C G A D F I H. E's neighbors B, C, G form level 1; A (from B), D (from C), and F (from G) form level 2; I (from A) and H (from D) form level 3. A is on level 2, two edges from E (E → B → A).On the same graph, what does recursive
dfs(F)print?Answer
F A B C D G E H I. From F the search descends to A, then A's first unvisited neighbor B, then C, D, G, and G's neighbor E. It backs up from E and G and visits H below D; finally, back at A, it visits A's last neighbor, I.What order does
kahn()return for the DAG with lists 0: [1, 2] · 1: [4] · 2: [4] · 3: [1] · 4: []?Answer
The in-degrees are
[0, 2, 1, 0, 2], so the queue starts with 0, 3. Removing 0 givesindeg[1] = 1andindeg[2] = 0, enqueuing 2; removing 3 givesindeg[1] = 0, enqueuing 1; removing 2 givesindeg[4] = 1; removing 1 givesindeg[4] = 0, enqueuing 4. The result is0 3 2 1 4; its size is 5 = n, so there is no cycle.On the graph with arrows 0 → 1, 1 → 2, 2 → 3, 3 → 1, while examining which arrow does
hasCycle(0, color)returntrue? What iscolorat that moment?Answer
0, 1, 2, 3 turn gray in turn:
color = [1, 1, 1, 1]. When 3 → 1 is examined, 1 is gray, on the current path; the arrow is a back edge, and the cycle found is 1 → 2 → 3 → 1.Run the Java Dijkstra from A on the undirected graph A–B 1, A–C 4, B–C 2, B–D 6, C–D 3. What are the final
dandprev? How many stale entries leave the queue?Answer
d = [0, 1, 3, 6](A, B, C, D) andprev = [–, A, B, C]. Processing A sets C to 4 and B to 1; processing B lowers C to 3 (adding{C, 3}) and sets D to 7; processing C lowers D to 6 (adding{D, 6}). The old entries{C, 4}and{D, 7}come out stale and are skipped: 2 stale entries.On the graph A–B 3, A–C 1, B–C 3, B–D 2, C–D 4, C–E 5, D–E 6, which edges does Kruskal choose, and what is the total weight? What changes if the tied edges A–B and B–C are tried in the other order?
Answer
In order: A–C 1 (accept), B–D 2 (accept), A–B 3 (accept, merging {A, C} and {B, D}), B–C 3 (reject), C–D 4 (reject), C–E 5 (accept). Four edges are chosen and D–E is never examined; the total is 1 + 2 + 3 + 5 = 11. Trying B–C first accepts B–C and rejects A–B: a different tree (A–C, B–D, B–C, C–E), still totaling 11.
On the graph of the previous question, in what order does lazy Prim from A choose edges (ties alphabetical), and how many stale entries does it skip?
Answer
A–C 1, A–B 3, B–D 2, C–E 5. After A–B is chosen, C–B 3 remains queued and is stale, since B is in the tree; when D joins, C–D 4 goes stale too. Both come out before C–E and are skipped: 2 stale entries. The total is again 11.
In a graph whose edges all weigh 5, which algorithm can replace Dijkstra for single-source shortest paths, and why?
Answer
BFS. With equal weights a path's length is 5 × number of edges, so the path with the fewest edges is also the shortest; multiply BFS's distances by 5. BFS runs in O(n + m), saving the log n factor of heap-based Dijkstra.
14.9Further reading
- M. T. Goodrich, R. Tamassia, and M. H. Goldwasser, Data Structures and Algorithms in Java (6th ed., 2014), Chapter 14: the graph ADT, representations, DFS and BFS, topological sorting, Dijkstra, Kruskal, and Prim, with Java code; the airport example comes from this chapter.
- R. Sedgewick and K. Wayne, Algorithms (4th ed., 2011), Chapter 4: undirected and directed graphs, MSTs, and shortest paths in separate sections, with a comparison of the lazy and eager versions of Prim.
- T. H. Cormen, C. E. Leiserson, R. L. Rivest, and C. Stein, Introduction to Algorithms (4th ed., 2022), Chapters 20–22: formal correctness proofs for BFS and DFS, the cut property, and the Bellman-Ford algorithm; the analysis of disjoint sets is in Chapter 19.
14.10References
- T. H. Cormen, C. E. Leiserson, R. L. Rivest, C. Stein, Introduction to Algorithms, 4th ed., MIT Press, 2022.
- E. W. Dijkstra, “A note on two problems in connexion with graphs,” Numerische Mathematik, 1, 269–271, 1959.
- M. T. Goodrich, R. Tamassia, M. H. Goldwasser, Data Structures and Algorithms in Java, 6th ed., Wiley, 2014.
- A. B. Kahn, “Topological sorting of large networks,” Communications of the ACM, 5(11), 558–562, 1962.
- J. B. Kruskal, “On the shortest spanning subtree of a graph and the traveling salesman problem,” Proceedings of the American Mathematical Society, 7(1), 48–50, 1956.
- R. C. Prim, “Shortest connection networks and some generalizations,” Bell System Technical Journal, 36(6), 1389–1401, 1957.
- R. Sedgewick, K. Wayne, Algorithms, 4th ed., Addison-Wesley, 2011.
Related pages: Chapter 14 slides · Study questions · Glossary · © Sercan Külcü. All rights reserved.