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.

Definition 14.1

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.

0 1 2 3 4 edge vertex
Figure 14.1: An undirected graph with five vertices, numbered 0 to 4, and seven edges. Circles are vertices; lines are edges.
Example 14.1

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.

Common mistake

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.

BLACK SEAMEDITERRANEANAEGEANEdirneIstanbulBursaIzmirEskişehirAnkaraAntalyaKonyaAdanaKayseriSamsunSivasGiresunTrabzonErzurumGaziantepDiyarbakırVan
Figure 14.2: A simplified drawing of the Turkish highway network. Cities are vertices; the main roads between them are edges.

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: A social network. People are vertices and friendships are edges. Colors mark tightly knit communities, with few edges between them.

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

Definition 14.2

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.

1 3 3 2 5 2 0 self-loop isolated vertex
Figure 14.4: Each vertex is labeled with its degree: 1, 3, 3, 2, 5, 2, and 0. The degree-5 vertex at the lower right has a self-loop; the degree-0 vertex on the left is isolated.

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.

Theorem 14.1 (Handshake lemma)

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.
Common mistake

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.

Definition 14.3

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.

A B C E D F
Figure 14.5: A directed graph with six vertices and the arrows A → B, B → C, C → E, E → D, D → B, and E → F.
Table 14.1: In- and out-degrees of the graph in Figure 14.5.
VertexABCDEFTotal
in-degree0211116
out-degree1111206

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.

Common mistake

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

Definition 14.4

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.

A B C D E F G H I J K L
Figure 14.6: Two simple cycles in a graph with twelve vertices: the green A-B-C-D-E-F-A and the dashed red G-H-I-J-K-L-G. The edges B–G, C–G, C–L, and D–K connect them, and the red cycle also has the chord K–I.

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.

1 2 3 4 5
Figure 14.7: The undirected example graph: V = {1, 2, 3, 4, 5}, E = {(1,2), (1,3), (1,4), (2,5), (3,4), (4,5)}.
Example 14.2

For the graph of Figure 14.7:

  1. Adjacency. 1 and 2 are neighbors because (1,2) is an edge; 2 and 3 are not.
  2. 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.
  3. Kinds of path. The table below classifies four sequences.
Table 14.2: Four vertex sequences in the graph of Figure 14.7.
SequenceKindReason
1 → 2 → 5simple pathedges 1–2, 2–5 exist; no repeats
1 → 3 → 4 → 1simple cycleends at 1; no repeats between
1 → 3 → 4 → 1 → 2non-simple pathall edges exist; 1 appears twice
1 → 3 → 4 → 1 → 4 → 1non-simple cycleends 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?

1 2 3 4 5
Figure 14.8: The directed example graph: V = {1, 2, 3, 4, 5}, E = {(1,2), (1,4), (2,5), (4,5), (3,1), (4,3)}.
Example 14.3

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

Definition 14.5

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.

Figure 14.9: Left to right: a connected graph, five vertices held together by four edges (1 component); a disconnected graph made of two vertices joined by an edge and two isolated vertices (3 components); the complete graph K5 (10 edges).

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.

Definition 14.6

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.

8 10 3 6 5 7 1 2 3 4 5 8 3 6 7 10 5 1 2 3 4 5
Figure 14.10: A weighted undirected graph (top) and a weighted directed graph (bottom) on the same five vertices. The undirected edges are 1–2: 8, 1–3: 10, 1–4: 3, 2–5: 6, 3–4: 5, 4–5: 7; the arrows are 1 → 2: 8, 1 → 4: 3, 2 → 5: 6, 4 → 5: 7, 3 → 1: 10, 4 → 3: 5.
Example 14.4

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.

Table 14.3: Basic operations of the graph ADT (V: vertex, E: edge).
OperationDescription
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.
A B C D E
Figure 14.11: The five-vertex undirected graph used to compare the representations, with edges A–B, A–E, B–C, B–E, C–D, and D–E.

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

Definition 14.7

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.

0 1 2 3 0 0 0 1 1 0 1 1 1 0 1 0 2 2 1 1 0 1 3 3 0 0 1 0 ← m[1][2]: one cell, O(1) ← neighbors of 2: whole row, O(n) symmetric: m[i][j] = m[j][i]
Figure 14.12: A four-vertex graph (edges 0–1, 0–2, 1–2, 2–3) and its adjacency matrix. Checking 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.

Example 14.5

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 B C D E 4 3 3 2 2 5 2 A B C D E
Figure 14.13: Left to right: the undirected graph (the graph of Figure 14.11); a directed graph with nine arrows (A → B, A → E, B → C, B → D, D → A, D → C, D → E, E → B, E → D); a weighted directed graph (A → B: 4, B → C: 3, B → D: 3, C → A: 2, C → E: 2, E → B: 5, E → D: 2).
Table 14.4: Adjacency matrix of the undirected graph.
ABCDE
A01001
B10101
C01010
D00101
E11010

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.

Table 14.5: Adjacency matrix of the directed graph (row = source, column = destination).
ABCDE
A01001
B00110
C00000
D10101
E01010

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.

Table 14.6: Adjacency matrix of the weighted directed graph (∞ for no edge, 0 on the diagonal).
ABCDE
A04∞∞∞
B∞033∞
C2∞0∞2
D∞∞∞0∞
E∞5∞20

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 ∞.

Common mistake

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

Definition 14.8

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.

0 1 2 3 0 1 2 1 0 2 2 0 1 3 3 2 ← edge 2–3? scan the list: O(deg 2) total entries 2m = 8 → space O(n + m)
Figure 14.14: Adjacency list of the four-vertex graph of Figure 14.12: 0: 1, 2 · 1: 0, 2 · 2: 0, 1, 3 · 3: 2. To ask whether edge 2–3 exists, scan 2's list: O(deg 2). There are 2m = 8 entries in all.

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.

A B E B A C E C B D D C E E A B D A B E B C D C (empty) D A C E E B D A B 4 B C 3 D 3 C A 2 E 2 D (empty) E B 5 D 2
Figure 14.15: Left to right: lists of the undirected graph (A: B, E · B: A, C, E · C: B, D · D: C, E · E: A, B, D); of the directed graph (A: B, E · B: C, D · C empty · D: A, C, E · E: B, D); of the weighted directed graph (A: (B, 4) · B: (C, 3), (D, 3) · C: (A, 2), (E, 2) · D empty · E: (B, 5), (D, 2)).
  • 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 an int[]{v, w} or a small Edge class.

14.2.6What each representation costs

Table 14.7: Memory and time costs of the three representations (n vertices, m edges).
OperationEdge listAdjacency listAdjacency matrix
MemoryO(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 neighborsO(m)O(deg v)O(n)
Add edgeO(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).
Example 14.6

Which representation suits a road network with n = 10,000 vertices and m = 30,000 edges?

  1. Matrix. n2 = 108 cells, of which 2m = 60,000 are filled in an undirected graph: 60,000 / 108 = 0.06%.
  2. 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).
  3. 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.
Common mistake

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(); }
}
  • adj is a List<List<Integer>>: one inner ArrayList (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. Like ArrayList.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) since ArrayList stores 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]);
Table 14.8: Neighbor lists after new Graph(5) and each of the six addEdge calls.
Step01234
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.

0 1 2 3 4 adj 0 [ ] 1 [ ] 2 [ ] 3 [ ] 4 [ ] 0 1 2 3 4 adj 0 1 1 0 2 [ ] 3 [ ] 4 [ ]
0 1 2 3 4 adj 0 1 4 1 0 2 4 2 1 3 [ ] 4 0 1 0 1 2 3 4 adj 0 1 4 1 0 2 4 2 1 3 3 2 4 4 0 1 3
Figure 14.16: Four moments while building the adjacency list. (a) After the constructor: five empty lists. (b) After 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).

Common mistake

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).

0 1 2 3 4 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 m[row][col] (1 = true)
Figure 14.17: The adjacency matrix after the six edges: 12 cells true, 13 false, symmetric about the diagonal.
Table 14.9: The final matrix (1 = true, 0 = false).
01234
001001
110101
201010
300101
411010

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

Definition 14.9

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.

Common mistake

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.

level 0 level 1 level 2 s a b c d e 1 2 3 4 5 6
Figure 14.18: BFS proceeds level by level. The number beside each vertex is its visiting order: s (1); a, b, c (2, 3, 4); d, e (5, 6). The bold edges form the BFS tree; the faint dashed edge b–d is not in it, because d was already reached through a.

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, all false.
  • Queue<Integer> q = new ArrayDeque<>(); creates an empty queue; add appends at the back, poll removes 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 u of w.

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.

Common mistake

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.

I A B C D H G F E q (front → back) (empty) visited[] A F B F C F D F E F F F G F H F I F unseen queued current done
Figure 14.19: The graph used in the traversal examples, in its initial state: the queue is empty and no vertex is visited. The strip below shows each vertex's visited[] entry.
Table 14.10: Trace of bfs(A). Each row is one round of the outer loop.
RoundRemoved w (printed)Neighbors scannedMarked and enqueuedQueue (end of round, front to back)
start——A[A]
1AB, F, IB, F, I (level 1)[B, F, I]
2BA, C, EC, E (level 2)[F, I, C, E]
3FA, GG (level 2)[I, C, E, G]
4IA—[C, E, G]
5CB, D, ED (level 3)[E, G, D]
6EB, C, G—[G, D]
7GD, E, F—[D]
8DC, G, HH (level 4)[H]
9HD—[]

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.

I A B C D H G F E q (front → back) B F I visited[] A T B T C F D F E F F T G F H F I T unseen queued current done I A B C D H G F E q (front → back) F I C E visited[] A T B T C T D F E T F T G F H F I T unseen queued current done
I A B C D H G F E q (front → back) E G D visited[] A T B T C T D T E T F T G T H F I T unseen queued current done I A B C D H G F E q (front → back) (empty) visited[] A T B T C T D T E T F T G T H T I T unseen queued current done
Figure 14.20: Four moments of 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.

s a b c d e 1 2 3 4 5 6
Figure 14.21: DFS goes deep along one path. Numbers give the visiting order: s (1), a (2), d (3), b (4); b is a dead end, so the search backs up; c (5), e (6). Bold edges form the DFS tree. Edge s–b is not in it, because b was reached through d.

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.

Table 14.11: Trace of recursive dfs(A). The call stack is written bottom to top.
StepEventCall stackPrinted (so far)
1dfs(A): A is markedAA
2A's neighbor B is new → dfs(B)A BA B
3B: A marked, skip; C new → dfs(C)A B CA B C
4C: skip B; D new → dfs(D)A B C DA B C D
5D: skip C; G new → dfs(G)A B C D GA B C D G
6G: skip D; E new → dfs(E)A B C D G EA B C D G E
7E: B, C, G marked → dfs(E) returnsA B C D G—
8G: F new → dfs(F)A B C D G FA B C D G E F
9F: A, G marked → returns; G has no neighbors left → returnsA B C D—
10D: H new → dfs(H)A B C D HA B C D G E F H
11H: skip D → returns; D done → returnsA B C—
12C: E marked → returns; B: E marked → returnsA—
13A: F marked, skip; I new → dfs(I)A IA B C D G E F H I
14I: 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.

I A B C D H G F E visited[] A T B T C T D T E T F F G T H F I F unseen on stack top output I A B C D H G F E visited[] A T B T C T D T E T F T G T H F I F unseen on stack top output
I A B C D H G F E visited[] A T B T C T D T E T F T G T H T I F unseen on stack top output I A B C D H G F E visited[] A T B T C T D T E T F T G T H T I T unseen on stack top output
Figure 14.22: Four moments of recursive 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.

Table 14.12: Trace of dfsStack(A). The stack is written bottom to top.
RoundpeekFirst unvisited neighborOperationStack (end of round)
start——push AA
1ABpush BA B
2BCpush CA B C
3CDpush DA B C D
4DGpush GA B C D G
5GEpush EA B C D G E
6EnonepopA B C D G
7GFpush FA B C D G F
8FnonepopA B C D G
9GnonepopA B C D
10DHpush HA B C D H
11HnonepopA B C D
12DnonepopA B C
13CnonepopA B
14BnonepopA
15AIpush IA I
16InonepopA
17Anonepop(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.

Common mistake

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

Table 14.13: BFS and DFS compared.
BFSDFS
Helper structurequeue (FIFO)stack (LIFO) or recursion
Time (adjacency list)O(n + m)O(n + m)
Extra memoryO(n): widest level in the queueO(n): longest path on the stack
Gives shortest paths?yes (unweighted graphs)no
Typical usefewest steps, finding levelscycle 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.

0 1 2 3 4 5 v comp 0 0 0 1 0 2 1 3 1 4 2 5
Figure 14.23: A six-vertex graph (edges 0–1, 0–2, 3–4) and the result of components. The three components are shown in three colors: comp = [0, 0, 0, 1, 1, 2].
Table 14.14: Trace of components on the graph of Figure 14.23.
scomp[s]Actioncountcomp (after)
——Arrays.fill0[−1, −1, −1, −1, −1, −1]
0−1dfs(0, 0): colors 0, 1, 21[0, 0, 0, −1, −1, −1]
10skip1same
20skip1same
3−1dfs(3, 1): colors 3, 42[0, 0, 0, 1, 1, −1]
41skip2same
5−1dfs(5, 2): 5 alone3[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

Definition 14.10

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.

0 1 2 3 4 5 0 1 2 bipartite ✓ (even cycles) triangle ✗: 1 and 2 same color
Figure 14.24: Left: a two-colorable six-vertex graph, the cycle 0–1–2–3–4–5–0 plus the edge 1–4. Right: a triangle; colored by BFS from 0, vertices 1 and 2 get the same color, and the edge between them exposes the conflict.
Example 14.7

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.

Definition 14.11

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 result
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
}

The Java code has three phases:

  1. Counting. Nested loops increment indeg at each arrow's head, in O(n + m).
  2. Initialization. Vertices of in-degree 0 are enqueued.
  3. 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: [].

1 2 0 3 4 5 indeg[] (in-degree) 0 1 2 3 4 5 indeg 1 0 0 2 2 2 q 1 2 result [ ]
Figure 14.25: The course prerequisite graph after the counting phase: 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.

Table 14.15: Trace of the main loop of kahn().
Removed uDeleted arrows and new in-degreesEnqueuedindeg (after)Queueresult
——1, 2[1, 0, 0, 2, 2, 2][1, 2][]
11 → 3: indeg[3] = 1; 1 → 4: indeg[4] = 1—[1, 0, 0, 1, 1, 2][2][1]
22 → 0: indeg[0] = 0; 2 → 4: indeg[4] = 00, 4[0, 0, 0, 1, 0, 2][0, 4][1, 2]
00 → 3: indeg[3] = 03[0, 0, 0, 0, 0, 2][4, 3][1, 2, 0]
44 → 5: indeg[5] = 1—[0, 0, 0, 0, 0, 1][3][1, 2, 0, 4]
33 → 5: indeg[5] = 05[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.

1 2 0 3 4 5 indeg[] (in-degree) 0 1 2 3 4 5 indeg 1 0 0 1 1 2 q (empty) result 1 2 1 2 0 3 4 5 indeg[] (in-degree) 0 1 2 3 4 5 indeg 0 0 0 1 0 2 q 0 4 result 1 2
1 2 0 3 4 5 indeg[] (in-degree) 0 1 2 3 4 5 indeg 0 0 0 0 0 1 q 3 result 1 2 0 4 1 2 0 3 4 5 indeg[] (in-degree) 0 1 2 3 4 5 indeg 0 0 0 0 0 0 q (empty) result 1 2 0 4 3 5
Figure 14.26: Four moments of 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).

Example 14.8

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.

Common mistake

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.

0 1 2 3 4 v color 1 0 2 1 2 2 1 3 1 4 0 white · 1 gray · 2 black back edge → CYCLE
Figure 14.27: Arrows 0 → 1, 1 → 2, 0 → 3, 3 → 2, 3 → 4, 4 → 0. When 4 → 0 is examined, 0 is gray, on the current path, so the cycle 0 → 3 → 4 → 0 is found. 1 and 2 are black.
Table 14.16: Trace of hasCycle(0, color).
StepEventcolor (0…4)
10 turns gray[1, 0, 0, 0, 0]
20 → 1: 1 white, call; 1 turns gray[1, 1, 0, 0, 0]
31 → 2: 2 white, call; 2 turns gray[1, 1, 1, 0, 0]
42 has no arrows: 2 black, false[1, 1, 2, 0, 0]
51's arrows done: 1 black, false[1, 2, 2, 0, 0]
60 → 3: 3 white, call; 3 turns gray[1, 2, 2, 1, 0]
73 → 2: 2 black, no cycle, continue[1, 2, 2, 1, 0]
83 → 4: 4 white, call; 4 turns gray[1, 2, 2, 1, 1]
94 → 0: 0 gray → back edge, true[1, 2, 2, 1, 1]
10true 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.

Common mistake

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

Definition 14.12

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.

Theorem 14.2 (BFS and shortest paths)

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. □

1 2 5 3 8 4 6 7 0 1 2 1 2 1 2 3
Figure 14.28: An unweighted graph with eight vertices and source 1. The edges are 1–2, 1–3, 1–4, 2–5, 3–8, 4–6, 5–8, 6–7, 7–8. The number beside each vertex is its distance from 1: 1 for 2, 3, 4; 2 for 5, 6, 8; 3 for 7.

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 dist

This 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.

  • INF stands for ∞ as Integer.MAX_VALUE.
  • Every distance starts at INF; the source gets 0 and is enqueued (offer appends, like add).
  • The main loop is BFS: each neighbor still at INF gets dist.get(w) + 1 and is enqueued.

The cost is BFS's, O(n + m), with HashMap operations O(1) on average.

Common mistake

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].

Table 14.17: Trace of shortestPath(1). The distance column lists the distances of vertices 1…8 in order (∞ = INF).
Removed wNeighborsUpdateddist (1…8)Queue
——dist[1] = 00 ∞ ∞ ∞ ∞ ∞ ∞ ∞[1]
12, 3, 42, 3, 4 ← 0 + 1 = 10 1 1 1 ∞ ∞ ∞ ∞[2, 3, 4]
21, 55 ← 1 + 1 = 20 1 1 1 2 ∞ ∞ ∞[3, 4, 5]
31, 88 ← 1 + 1 = 20 1 1 1 2 ∞ ∞ 2[4, 5, 8]
41, 66 ← 1 + 1 = 20 1 1 1 2 2 ∞ 2[5, 8, 6]
52, 8— (both known)0 1 1 1 2 2 ∞ 2[8, 6]
83, 5, 77 ← 2 + 1 = 30 1 1 1 2 2 3 2[6, 7]
64, 7— (both known)0 1 1 1 2 2 3 2[7]
76, 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.

1 2 5 3 8 4 6 7 1 2 3 4 5 6 7 8 dist 0 1 1 1 ∞ ∞ ∞ ∞ q 2 3 4 1 2 5 3 8 4 6 7 1 2 3 4 5 6 7 8 dist 0 1 1 1 2 2 3 2 q 6 7
Figure 14.29: Two moments of 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.

4 2 5 10 3 4 11 A B D C E F
Figure 14.30: A weighted directed graph with arrows A → B: 4, A → C: 2, B → C: 5, B → D: 10, C → E: 3, E → D: 4, D → F: 11. The shortest path from A to F is A → C → E → D → F: 2 + 3 + 4 + 11 = 20.
Example 14.9

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:

  1. picks, among vertices not yet settled, the one with the smallest tentative distance, whose distance is now final;
  2. updates its neighbors' distances by relaxation.
Definition 14.13

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 Q

Q 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.

Common mistake

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 as int[]{v, w} pairs, neighbor and weight. The caller allocates prev; dijkstra fills it.
  • Setup. The long array d is filled with Long.MAX_VALUE, standing for ∞; d[s] = 0; prev is filled with −1, “no predecessor.”
  • Queue. Entries are {vertex, distance} pairs; the comparator Long.compare(a[1], b[1]) orders by distance, so poll returns 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]; if alt < d[v], the distance and prev are 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.

Common mistake

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.

3 4 7 1 2 3 7 2 2 3 5 I A B C D H G F E A B C D E F G H I d[] 0 ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ prev – – – – – – – – – pq (shown by distance; faded = stale) A,0
Figure 14.31: The start of the Dijkstra trace: 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.

Table 14.18: Trace of dijkstra(adj, A).
Removed entryStatusRelaxationsQueue (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] = 11skipped{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.

3 4 7 1 2 3 7 2 2 3 5 I A B C D H G F E A B C D E F G H I d[] 0 4 5 12 6 7 ∞ ∞ 3 prev – A B C B A – – A pq (shown by distance; faded = stale) E,6 F,7 D,12 3 4 7 1 2 3 7 2 2 3 5 I A B C D H G F E A B C D E F G H I d[] 0 4 5 11 6 7 8 ∞ 3 prev – A B G B A E – A pq (shown by distance; faded = stale) D,11 D,12
3 4 7 1 2 3 7 2 2 3 5 I A B C D H G F E 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 pq (shown by distance; faded = stale) H,16 3 4 7 1 2 3 7 2 2 3 5 I A B C D H G F E 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 pq (shown by distance; faded = stale) (empty)
Figure 14.32: Four moments of the Dijkstra trace. (a) C is settled; 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.
Table 14.19: Dijkstra's result (source A).
ABCDEFGHI
d04511678163
prev–ABGBAEDA

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.

Example 14.10

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:

  1. v = F: list [F]; v = prev[F] = D.
  2. v = D: list [D, F]; v = prev[D] = E.
  3. v = E: list [E, D, F]; v = prev[E] = C.
  4. v = C: list [C, E, D, F]; v = prev[C] = A.
  5. 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.

4 2 5 10 3 4 11 A B D C E F v prev – A A B A C E D C E D F p A C E D F addFirst → from target to source, prepend
Figure 14.33: The end of path reconstruction. The prev chain has been followed from F to A, each vertex added to the front of the list: p = [A, C, E, D, F].
Common mistake

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:

Table 14.20: Two implementations of Dijkstra's algorithm.
ImplementationComplexityWhy?
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.

Theorem 14.3 (Correctness of Dijkstra's algorithm)

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.

Example 14.11

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

Definition 14.14

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.

0 1 1 1 1 2 2 2 2 4 4 4 4 5 5 6 7 11 A B C D E F G H I J
Figure 14.34: An undirected graph with ten vertices and eighteen weighted edges. By weight: I–J 0; A–E, C–I, E–F, G–H 1; B–D, C–J, D–E, D–H 2; A–D, B–C, C–H, G–I 4; A–B, D–F 5; H–I 6; F–G 7; D–G 11.

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.

Theorem 14.4 (Cut 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 when find gives them the same representative.
  • union(u, v) merges the sets of u and v and returns true if they differ; if they are already the same set, it does nothing and returns false.

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. find links 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.

Table 14.21: Cost of Kruskal's algorithm.
PhaseCost
Sorting the edgesO(m log m)
2m find + (n − 1) unionO(m · α(n))
TotalO(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.

Table 14.22: The edges Kruskal tries, and its decisions.
EdgeWeightDecisionSets merged / reason|T|Total
I–J0accept{I} + {J}10
A–E1accept{A} + {E}21
C–I1accept{C} + {I, J}32
E–F1accept{A, E} + {F}43
G–H1accept{G} + {H}54
B–D2accept{B} + {D}66
C–J2rejectC and J already in {C, I, J}66
D–E2accept{B, D} + {A, E, F}78
D–H2accept{A, B, D, E, F} + {G, H}810
A–D4rejectA and D in the same set810
B–C4acceptlarge set + {C, I, J}914

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.

I–J = 0 ✓ A–E = 1 ✓ C–I = 1 ✓ E–F = 1 ✓ G–H = 1 ✓ B–D = 2 ✓ C–J = 2 ✗ D–E = 2 D–H = 2 A–D = 4 B–C = 4 C–H = 4 G–I = 4 A–B = 5 D–F = 5 H–I = 6 F–G = 7 D–G = 11 0 1 1 1 1 2 2 2 2 4 4 4 4 5 5 6 7 11 A B C D E F G H I J Chosen edge 6 / 9 Total weight 6
I–J = 0 ✓ A–E = 1 ✓ C–I = 1 ✓ E–F = 1 ✓ G–H = 1 ✓ B–D = 2 ✓ C–J = 2 ✗ D–E = 2 ✓ D–H = 2 ✓ A–D = 4 ✗ B–C = 4 ✓ C–H = 4 G–I = 4 A–B = 5 D–F = 5 H–I = 6 F–G = 7 D–G = 11 0 1 1 1 1 2 2 2 2 4 4 4 4 5 5 6 7 11 A B C D E F G H I J Chosen edge 9 / 9 Total weight 14
Figure 14.35: Two moments of Kruskal. Top: C–J is rejected, since C and J are in the same tree. Bottom: the final state, with the nine chosen edges A–E, E–F, D–E, B–D, D–H, G–H, B–C, C–I, I–J and total weight 14. In the list on the left, accepted edges are checked and rejected ones struck through.
Common mistake

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.

Table 14.23: The Union-Find forest during kruskal().
Edgefind rootsunionLinkedParents changed
I–J (0)I, JtrueI → J (equal sizes)I → J
A–E (1)A, EtrueA → E (equal)A → E
C–I (1)C, JtrueC → J (1 < 2)C → J
E–F (1)E, FtrueF → E (1 < 2)F → E
G–H (1)G, HtrueG → H (equal)G → H
B–D (2)B, DtrueB → D (equal)B → D
C–J (2)J, Jfalse——
D–E (2)D, EtrueD → E (2 < 3)D → E
D–H (2)E, HtrueH → E (2 < 5)H → E
A–D (4)E, Efalse——
B–C (4)E, JtrueJ → 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.

A B C D E F G H I J edge (sorted) I–J 0 ✓ A–E 1 ✓ C–I 1 ✓ E–F 1 ✓ G–H 1 ✓ B–D 2 ✓ C–J 2 ✗ D–E 2 ✓ D–H 2 A–D 4 B–C 4 C–H 4 G–I 4 A–B 5 D–F 5 H–I 6 F–G 7 D–G 11 uf forest (arrow → parent) A B C D E F G H I J |T|=7, weight=8 A B C D E F G H I J edge (sorted) I–J 0 ✓ A–E 1 ✓ C–I 1 ✓ E–F 1 ✓ G–H 1 ✓ B–D 2 ✓ C–J 2 ✗ D–E 2 ✓ D–H 2 ✓ A–D 4 ✗ B–C 4 ✓ C–H 4 G–I 4 A–B 5 D–F 5 H–I 6 F–G 7 D–G 11 uf forest (arrow → parent) A B C D E F G H I J |T|=9, weight=14
Figure 14.36: Two moments of the Union-Find forest (arrows point to parents). (a) D–E is accepted: D's tree goes under E. (b) Final state: one tree with root E. Path compression has pointed B's arrow directly at E.

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.

2704 867 849 740 621 1846 802 144 187 184 1258 337 1464 1235 1391 946 1090 1121 2342 BOS PVD ORD JFK BWI SFO LAX DFW MIA
Figure 14.37: Nine airports and 19 routes. The bold edges are the MST found by Kruskal: JFK–PVD 144, JFK–BWI 184, JFK–BOS 187, SFO–LAX 337, ORD–BWI 621, ORD–DFW 802, BWI–MIA 946, LAX–DFW 1235. Dashed edges are not chosen.
Table 14.24: Kruskal's decisions in the airport example.
Route (miles)DecisionReason
JFK–PVD 144, JFK–BWI 184, JFK–BOS 187acceptfour airports join around JFK
SFO–LAX 337accepta separate tree in the west
ORD–BWI 621acceptORD joins the eastern tree
ORD–JFK 740rejectORD and JFK already connected via BWI (cycle)
ORD–DFW 802acceptDFW joins the eastern tree
ORD–PVD 849, ORD–BOS 867rejectboth ends in the eastern tree
BWI–MIA 946acceptMIA joins the eastern tree
JFK–MIA 1090, DFW–MIA 1121rejectboth ends in the eastern tree
LAX–DFW 1235acceptwestern 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 T

Prim 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.

  1. The start s joins, and its edges are enqueued.
  2. Until the queue empties or n − 1 edges are chosen, take the lightest entry.
  3. If candidate e[1] is in the tree, the entry is stale (it would close a cycle), and continue skips it.
  4. 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.

Common mistake

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.

Table 14.25: Prim from A (ties broken alphabetically).
StepChosen edgeJoins treeNew candidates enqueuedTotal
0—AA–E 1, A–D 4, A–B 50
1A–E (1)EE–D 2, E–F 11
2E–F (1)FF–D 5, F–G 72
3E–D (2)DD–B 2, D–G 11, D–H 24
4D–B (2)BB–C 46
5D–H (2)HH–C 4, H–G 1, H–I 68
6H–G (1)GG–I 49
7B–C (4) (stale A–D 4 skipped first)CC–I 1, C–J 213
8C–I (1)II–J 014
9I–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.

priority queue D–B = 2 D–H = 2 A–D = 4 A–B = 5 F–D = 5 F–G = 7 D–G = 11 0 1 1 1 1 2 2 2 2 4 4 4 4 5 5 6 7 11 A B C D E F G H I J Tree vertex 4 / 10 Total weight 4
priority queue C–J = 2 H–C = 4 A–D = 4 G–I = 4 A–B = 5 F–D = 5 H–I = 6 F–G = 7 D–G = 11 0 1 1 1 1 2 2 2 2 4 4 4 4 5 5 6 7 11 A B C D E F G H I J Tree vertex 10 / 10 Total weight 14
Figure 14.38: Two moments of Prim. Top: E–D is chosen and D joins the tree; struck-through entries have gone stale. Bottom: the final state, ten vertices in the tree, total weight 14.

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.

Example 14.12

Start Prim at J on the same graph (ties alphabetical).

  1. J–I 0 and J–C 2 are enqueued; J–I brings in I, adding I–C 1, I–G 4, I–H 6.
  2. I–C 1 brings in C, adding C–B 4 and C–H 4.
  3. 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

Table 14.26: Kruskal's and Prim's algorithms compared.
KruskalPrim
Ideatries edges lightest first, merging treesgrows one tree from a start vertex
Intermediate stateforest (several trees)one tree
Data structuressorted edge list and Union-Findpriority queue, mark array, adjacency list
Cycle checkfind(u) == find(v)is v already in the tree?
CostO(m log n) (sorting dominates)O(m log n) with a heap; O(n2) with a matrix
When?sparse graph, edges given as a listadjacency 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.

Common mistake

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.
Table 14.27: The algorithms of this chapter.
AlgorithmHelper structureTime (with adjacency lists)Produces
BFSqueueO(n + m)level order, unweighted shortest paths
DFSstack / recursionO(n + m)depth-first traversal, cycles, components
Topological sort (Kahn)queue / DFSO(n + m)dependency order for a DAG
Dijkstrapriority queueO((n + m) log n)weighted shortest paths (weights ≥ 0)
Kruskalsorting and Union-FindO(m log n)minimum spanning tree
Primpriority queueO(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.

14.8Test Yourself

  1. 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).

  2. 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.

  3. 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).

  4. 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.

  5. 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 gives indeg[1] = 1 and indeg[2] = 0, enqueuing 2; removing 3 gives indeg[1] = 0, enqueuing 1; removing 2 gives indeg[4] = 1; removing 1 gives indeg[4] = 0, enqueuing 4. The result is 0 3 2 1 4; its size is 5 = n, so there is no cycle.

  6. On the graph with arrows 0 → 1, 1 → 2, 2 → 3, 3 → 1, while examining which arrow does hasCycle(0, color) return true? What is color at 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.

  7. 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 d and prev? How many stale entries leave the queue?

    Answer

    d = [0, 1, 3, 6] (A, B, C, D) and prev = [–, 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.

  8. 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.

  9. 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.

  10. 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

  1. T. H. Cormen, C. E. Leiserson, R. L. Rivest, C. Stein, Introduction to Algorithms, 4th ed., MIT Press, 2022.
  2. E. W. Dijkstra, “A note on two problems in connexion with graphs,” Numerische Mathematik, 1, 269–271, 1959.
  3. M. T. Goodrich, R. Tamassia, M. H. Goldwasser, Data Structures and Algorithms in Java, 6th ed., Wiley, 2014.
  4. A. B. Kahn, “Topological sorting of large networks,” Communications of the ACM, 5(11), 558–562, 1962.
  5. 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.
  6. R. C. Prim, “Shortest connection networks and some generalizations,” Bell System Technical Journal, 36(6), 1389–1401, 1957.
  7. R. Sedgewick, K. Wayne, Algorithms, 4th ed., Addison-Wesley, 2011.

Related pages: Chapter 14 slides · Study questions · Glossary · © Sercan Külcü. All rights reserved.