Graph Algorithms

Study Sheet

Graph Algorithms

Searching a graph, shortest paths, and minimum spanning trees

Breadth-First and Depth-First Search

Concept
Two ways to explore a graph

Breadth-first search (BFS) from a vertex ss visits vertices in order of their distance from ss: it keeps a queue, starts with ss at distance 00, and whenever it dequeues a vertex uu it enqueues every unvisited neighbor of uu at distance d(u)+1d(u) + 1. Depth-first search (DFS) instead dives as deep as possible before backing up: from uu it recurses into an unvisited neighbor, and only returns to try uu's next neighbor once that whole branch is exhausted. Both visit every vertex and edge once, using O(V+E)O(V + E) time with an adjacency list.

Both algorithms share one invariant: once a vertex is marked visited it is never processed again, so the running time stays linear. BFS processes vertices first-in-first-out, which forces it to finish an entire distance layer before starting the next — this is exactly why the tree of edges BFS builds (a spanning tree of the component, Topic 4) records true shortest-path distances in an unweighted graph. DFS processes last-in-first-out instead, plunging down one path as far as it goes; the price is that its tree looks nothing like a shortest-path tree, but the reward is that every non-tree edge DFS meets connects a vertex to one of its OWN ancestors — never a stranger in a different branch — which is what makes DFS the standard tool for finding cycles. Which neighbor order a search scans first never changes which vertices get visited, only the shape of the resulting tree.

Example
BFS from s
sabcdef
BFS tree from s (bold); every layer is one more edge away.

The graph has vertices s,a,b,c,d,e,fs, a, b, c, d, e, f and edges sa,sb,ac,ad,bd,be,cf,df,efsa, sb, ac, ad, bd, be, cf, df, ef, so ∣E∣=9|E| = 9. Scanning each vertex's neighbors in alphabetical order: dequeuing ss enqueues a,ba, b at distance 11; dequeuing aa enqueues c,dc, d at distance 22 (ss is already visited); dequeuing bb enqueues ee at distance 22 (dd is already queued); dequeuing cc enqueues ff at distance 33. The remaining dequeues (dd, ee, ff) find every neighbor already visited. So d(s,f)=3d(s,f) = 3, reached as s→a→c→fs \to a \to c \to f, and the BFS tree is sa,sb,ac,ad,be,cfsa, sb, ac, ad, be, cf — six edges for seven vertices, as a spanning tree (Topic 4) must have.

Example
DFS from s
sabcdef
DFS tree from s (bold) with its three back edges (dashed).

Explore the same graph with DFS, again scanning neighbors alphabetically. From ss go to aa; from aa go to cc (before dd); from cc go to ff; from ff, skip its own parent cc and go to dd; from dd, its neighbor aa is already visited and is an ancestor, giving the back edge adad, so go on to bb; from bb go to ee; from ee, both remaining neighbors are already visited — bb is its parent, and ff is an ancestor, giving the back edge efef. Backing up to bb, then dd, then ff, then cc, then aa finds nothing new; backing up to ss, its neighbor bb is visited and is an ancestor, giving the third back edge sbsb. The DFS order is s,a,c,f,d,b,es, a, c, f, d, b, e, the DFS tree is sa,ac,cf,fd,db,besa, ac, cf, fd, db, be, and the three back edges adad, efef, sbsb make up the remaining 9−6=39 - 6 = 3 edges.

Example
A cycle from a back edge

Does this graph contain a cycle? A tree on 77 vertices has exactly 66 edges, and this graph has 99, which already forces one (Topic 4). The DFS back edge efef exhibits one directly: the tree path from ff down to ee is f→d→b→ef \to d \to b \to e, and the back edge closes it, giving the cycle f,d,b,e,ff, d, b, e, f. Each of its edges — fd,db,be,effd, db, be, ef — really is in the graph, so this is a genuine 44-cycle, not just a count.

Side note
Where it goes wrong

BFS finds the path with the fewest EDGES, not the cheapest one — the two agree only when every edge costs the same. Give a direct edge stst weight 1010 and a two-hop route s→a→ts \to a \to t weight 1+1=21 + 1 = 2 each: BFS, which ignores weights entirely, reports the 11-edge path stst as "shortest", but its true cost 1010 is five times the two-hop route's cost 22. The next section fixes exactly this blind spot.

Try it
Try it: BFS distance

Find the BFS distance from uu to yy in the graph with edges uv,uw,vx,wx,wyuv, uw, vx, wx, wy.

Show the answer

Scanning alphabetically from uu: dequeuing uu enqueues v,wv, w at distance 11; dequeuing vv enqueues xx at distance 22; dequeuing ww enqueues yy at distance 22 (xx is already queued). So d(u,y)=2d(u,y) = \mathbf{2}, achieved by u→w→yu \to w \to y.

Shortest Paths: Dijkstra's Algorithm

Concept
Dijkstra's algorithm

Dijkstra's algorithm finds shortest-path distances from a source ss in a graph with non-negative edge weights. Maintain a tentative distance δ(v)\delta(v) for every vertex (δ(s)=0\delta(s) = 0, δ(v)=∞\delta(v) = \infty otherwise) and a set of finalized vertices. Repeatedly pick the unfinalized vertex uu with the smallest δ(u)\delta(u), finalize it, and relax every edge uvuv: if δ(u)+w(uv)<δ(v)\delta(u) + w(uv) < \delta(v), replace δ(v)\delta(v) by δ(u)+w(uv)\delta(u) + w(uv). Once finalized, a vertex's distance never changes again.

The key claim, proved in Section 4, is that the unfinalized vertex with smallest tentative distance already holds its TRUE shortest distance, so finalizing it is always safe and never needs revisiting. This is BFS generalized: BFS finalizes vertices in order of hop-count because every edge there secretly has weight 11, while Dijkstra finalizes them in order of total weight, using a priority queue instead of a plain queue to find the minimum quickly. Non-negative weights are essential: they guarantee that continuing a finalized vertex's path by one more edge can never make a currently-longer route cheaper than what is already finalized — exactly what fails with negative weights, below.

Example
Dijkstra from s, start to finish
7914101511269sabcde
Shortest-path tree from s (bold); the route to e is s, b, c, e.

The graph has vertices s,a,b,c,d,es,a,b,c,d,e and weighted edges sa:7,sb:9,sc:14,ab:10,ad:15,bd:11,bc:2,de:6,ce:9sa{:}7, sb{:}9, sc{:}14, ab{:}10, ad{:}15, bd{:}11, bc{:}2, de{:}6, ce{:}9. Initialize δ(s)=0\delta(s)=0, everything else ∞\infty. Finalize ss and relax: δ(a)=7,δ(b)=9,δ(c)=14\delta(a)=7,\delta(b)=9,\delta(c)=14. Smallest tentative is aa at 77; finalize it and relax: adad gives 7+15=227+15=22, so δ(d)=22\delta(d)=22 (abab gives 17>917 > 9, no change). Smallest is now bb at 99; finalize it and relax: bcbc gives 9+2=11<149+2=11 < 14, so δ(c)=11\delta(c)=11; bdbd gives 9+11=20<229+11=20 < 22, so δ(d)=20\delta(d)=20 — a distance can improve twice before it is finalized. Smallest is now cc at 1111; finalize it and relax: cece gives 11+9=2011+9=20, so δ(e)=20\delta(e)=20. The last two, dd and ee, are tied at 2020 and finalizing either changes nothing further. Final distances: δ(a)=7,δ(b)=9,δ(c)=11,δ(d)=20,δ(e)=20\delta(a)=7,\delta(b)=9,\delta(c)=11,\delta(d)=20,\delta(e)=20, and the shortest route to ee is s→b→c→es \to b \to c \to e, cost 9+2+9=209+2+9=20 — not the two-edge route through aa and dd.

Example
A shortcut changes the best route

Suppose a direct road aeae is built, weight 1111. After finalizing ss (distance 00) and aa (distance 77), relaxing aeae gives a tentative δ(e)=7+11=18\delta(e) = 7 + 11 = 18, already below the old answer of 2020. Finalizing continues through bb (99) and cc (1111); relaxing cece gives 11+9=2011+9=20, which no longer improves 1818. So ee finalizes next at δ(e)=18\delta(e) = 18: the new shortest path is s→a→es \to a \to e, cost 1818, beating the old s→b→c→es \to b \to c \to e at cost 2020.

Side note
Where it goes wrong
12−5sab
A negative edge lets the 2-edge route beat the 1-edge route Dijkstra already finalized.

Dijkstra's greedy step assumes that once a vertex is finalized, nothing discovered later can beat its distance — true only when weights are non-negative. Take the directed graph s→bs \to b weight 11, s→as \to a weight 22, a→ba \to b weight −5-5. Dijkstra finalizes ss (distance 00) then bb (distance 11, the smaller tentative value) before ever finalizing aa, and never revisits a finalized vertex — so it reports δ(b)=1\delta(b) = 1. But the true shortest distance is via aa: 2+(−5)=−32 + (-5) = -3, cheaper than 11. Negative weights call for a different algorithm (Bellman-Ford), not Dijkstra.

Try it
Try it: a different finalized distance

Using the first graph above (without the aeae shortcut), find δ(c)\delta(c) and the path achieving it.

Show the answer

Finalizing in order ss (0), aa (7), bb (9): relaxing bcbc gives 9+2=119 + 2 = 11, better than the 1414 that scsc gives directly, and no later relaxation improves it. So δ(c)=11\delta(c) = \mathbf{11}, via s→b→cs \to b \to c.

Minimum Spanning Trees: Kruskal and Prim

Concept
Kruskal's and Prim's algorithms

A minimum spanning tree (MST) of a connected weighted graph is a spanning tree (Topic 4) whose edge weights sum to the least total possible. Kruskal's algorithm sorts all edges by weight and adds each one unless it would close a cycle with edges already chosen. Prim's algorithm instead grows a single tree from one vertex, at each step adding the cheapest edge leaving the current tree to a vertex not yet in it.

Both are greedy — neither ever reconsiders a choice — yet both are provably optimal (Section 4), which is unusual: most greedy strategies for optimization problems fail. Kruskal thinks edge-first, testing "would this close a cycle" with a union-find structure; Prim thinks vertex-first, using a priority queue exactly like Dijkstra's but keyed on edge weight to the tree rather than distance from a source, which is also why Prim is easy to run on a graph given only as an adjacency structure with no global edge list. When every edge weight is distinct, the MST is unique, and both algorithms must find the very same tree.

Example
Kruskal and Prim agree
41372568pqrstu
The minimum spanning tree (bold), weight 19.

The graph has vertices p,q,r,s,t,up,q,r,s,t,u and weighted edges pq:4,pr:1,qr:3,qs:7,rs:2,rt:5,st:6,tu:8pq{:}4, pr{:}1, qr{:}3, qs{:}7, rs{:}2, rt{:}5, st{:}6, tu{:}8 — eight edges, so a spanning tree needs 6−1=56-1=5. Kruskal sorts them pr(1),rs(2),qr(3),pq(4),rt(5),st(6),qs(7),tu(8)pr(1), rs(2), qr(3), pq(4), rt(5), st(6), qs(7), tu(8) and scans in order: add prpr; add rsrs; add qrqr; skip pqpq (pp and qq already joined, this would close a cycle); add rtrt; skip stst (cycle); skip qsqs (cycle); add tutu. The tree is pr,rs,qr,rt,tupr, rs, qr, rt, tu, total weight 1+2+3+5+8=191+2+3+5+8=19. Running Prim from pp instead — always grabbing the cheapest edge leaving the current tree — adds pr(1)pr(1), then rs(2)rs(2), then qr(3)qr(3), then rt(5)rt(5), then tu(8)tu(8): the identical set of edges, confirming the MST (unique here, since every weight differs) has weight 1919.

Example
Raising a bridge's cost
41372968pqrstu
Raising rt to 9 swaps it for st (bold); the new tree weighs 20.

Suppose road rtrt rises from weight 55 to weight 99 — does the minimum spanning tree change? Re-sorting gives pr(1),rs(2),qr(3),pq(4),st(6),qs(7),tu(8),rt(9)pr(1), rs(2), qr(3), pq(4), st(6), qs(7), tu(8), rt(9). Kruskal still adds pr,rs,qrpr, rs, qr; skips pqpq (cycle); now stst at 66 is cheaper than the alternative rtrt at 99 for reaching tt, so stst is added; skips qsqs (cycle); adds tutu; never needs rtrt (it would only close a cycle). The new tree is pr,rs,qr,st,tupr, rs, qr, st, tu, total weight 1+2+3+6+8=201+2+3+6+8=20: edge rtrt leaves the tree and stst takes its place, since 6<96 < 9.

Side note
Where it goes wrong

The MST minimizes the TOTAL weight of the tree; it is not the same as the shortest-path tree from some vertex, which instead minimizes each root-to-vertex distance on its own. On the Dijkstra graph from the last section, the MST is bc,de,sa,sb,cebc, de, sa, sb, ce, total weight 2+6+7+9+9=332+6+7+9+9=33; the shortest-path tree from ss is instead sa,sb,bc,bd,cesa, sb, bc, bd, ce, total weight 7+9+2+11+9=387+9+2+11+9=38 — more expensive overall, because it insists on giving dd the shortest possible distance from ss (2020, via bb) rather than the cheapest possible edge into dd (dede, weight 66).

Try it
Try it: a small MST

Find the minimum spanning tree weight for the graph with edges wx:3,wy:1,xy:4,xz:2,yz:5wx{:}3, wy{:}1, xy{:}4, xz{:}2, yz{:}5.

Show the answer

Sort ascending: wy(1),xz(2),wx(3),xy(4),yz(5)wy(1), xz(2), wx(3), xy(4), yz(5). Add wywy (joins w,yw,y); add xzxz (joins x,zx,z); add wxwx, which connects the two pieces into one — three edges already span all four vertices, so stop. Total weight 1+2+3=61+2+3=\mathbf{6}.

Why Greedy Works

A cut is any partition of the vertices into two nonempty sets SS and V∖SV \setminus S; an edge crosses the cut if it has one endpoint in each. Cut property: if ee is the strict minimum-weight edge crossing some cut, then ee belongs to every minimum spanning tree. Proof: let TT be any spanning tree not containing e=uve = uv (with u∈Su \in S, v∉Sv \notin S). Adding ee to TT creates a cycle — the new edge plus the unique tree path already joining uu and vv — and since that cycle starts in SS and ends outside it, the cycle must cross the cut again at some OTHER edge f≠ef \ne e. Removing ff from T+eT + e restores a spanning tree T′=T+e−fT' = T + e - f, since ff lay on the cycle that ee created and nothing else is disturbed. Because ee is the strict minimum among all cut-crossing edges, w(e)<w(f)w(e) < w(f), so w(T′)<w(T)w(T') < w(T): no spanning tree omitting ee can be minimum, so every MST contains ee.

Assume, as in the examples above, that all edge weights are distinct, so the MST is unique. Kruskal processes edges in increasing order e1<e2<⋯e_1 < e_2 < \cdots and adds ei=uve_i = uv exactly when uu and vv lie in different components of the forest built so far. Claim: every edge Kruskal adds belongs to the MST. Fix such an eie_i, and let SS be the component containing uu at that moment (built entirely from edges lighter than eie_i). Then v∉Sv \notin S, so eie_i crosses the cut (S,V∖S)(S, V \setminus S). No edge ff crossing this cut can be lighter than eie_i: such an ff would have been examined earlier, and either it was rejected because its endpoints already shared a component — but components only ever merge, never split, so they would still share one now, putting both of ff's endpoints in SS and contradicting that ff crosses the cut — or it was accepted, which merges its endpoints into one component from then on, again putting both inside SS now, the same contradiction. So eie_i is the unique minimum-weight edge crossing (S,V∖S)(S, V \setminus S), and the cut property forces it into the MST. Kruskal only ever adds edges forced into the MST, and it adds exactly n−1n - 1 of them forming a spanning tree, so its output IS the MST.

Run Prim from any start vertex rr, and let SS be the vertex set of the tree built so far at any moment (initially S={r}S = \{r\}). By definition, Prim's next step adds the minimum-weight edge crossing the cut (S,V∖S)(S, V \setminus S) — scanning every edge leaving SS for the cheapest one IS Prim's selection rule. With distinct weights, that edge is the unique minimum crossing the cut, so the cut property places it in the MST. Every edge Prim ever adds is therefore an edge of the MST, and since the graph is connected, SS always has an edge leaving it until S=VS = V; Prim halts after adding exactly n−1n - 1 such edges, producing a spanning tree made entirely of MST edges — which must BE the MST, since the MST itself has only n−1n - 1 edges.

Claim: whenever Dijkstra finalizes a vertex uu, its tentative distance equals the true shortest distance, δ(u)=d(s,u)\delta(u) = d(s, u). Proof by induction on the order of finalization. The first vertex finalized is ss, with δ(s)=0=d(s,s)\delta(s) = 0 = d(s,s). For the inductive step, suppose every vertex finalized before uu already satisfies the claim, and consider a true shortest path PP from ss to uu. Let xx be the first vertex along PP not yet finalized (possibly x=ux = u), and yy its predecessor on PP (or y=sy = s). Every vertex before xx on PP, including yy, is already finalized correctly, so when yy was finalized, Dijkstra relaxed edge yxyx and set δ(x)≤δ(y)+w(yx)=d(s,y)+w(yx)\delta(x) \le \delta(y) + w(yx) = d(s,y) + w(yx). Since the prefix of a shortest path is itself a shortest path to any vertex along it, d(s,y)+w(yx)=d(s,x)d(s,y) + w(yx) = d(s,x), giving δ(x)≤d(s,x)\delta(x) \le d(s,x). Because no edge weight is negative, continuing along PP from xx to uu cannot shorten anything, so d(s,x)≤d(s,u)d(s,x) \le d(s,u); and δ(u)\delta(u), being the length of some real walk from ss to uu, always satisfies d(s,u)≤δ(u)d(s,u) \le \delta(u). Chaining these: δ(x)≤d(s,x)≤d(s,u)≤δ(u)\delta(x) \le d(s,x) \le d(s,u) \le \delta(u). But Dijkstra chose to finalize uu precisely because δ(u)\delta(u) was smallest among unfinalized vertices, and xx is unfinalized too, so δ(u)≤δ(x)\delta(u) \le \delta(x). Every inequality in the chain is now forced to equality, in particular d(s,u)=δ(u)d(s,u) = \delta(u). Non-negativity is used exactly once, in d(s,x)≤d(s,u)d(s,x) \le d(s,u) — precisely the step that fails in the counterexample of Section 2.

Directed Acyclic Graphs and Topological Order

Concept
DAGs and topological order

A directed acyclic graph (DAG) is a directed graph with no directed cycle. A topological order of a digraph is an ordering of its vertices so that every edge u→vu \to v has uu before vv. Theorem: a digraph has a topological order if and only if it is a DAG.

The "only if" direction is immediate: if u1→u2→⋯→uk→u1u_1 \to u_2 \to \cdots \to u_k \to u_1 is a cycle, no linear order can satisfy every edge, since the last edge demands uku_k before u1u_1 while the rest of the chain demands u1u_1 before uku_k. The "if" direction is constructive and gives an algorithm, Kahn's algorithm: repeatedly remove a vertex with no remaining incoming edge and append it to the order; one such vertex must always exist in a DAG, since following edges backward forever without repeating a vertex is impossible once the vertices run out, and repeating one would mean a cycle. A DAG's topological order is rarely unique: whenever two unfinished vertices both have no remaining incoming edges, either may be chosen next, so unrelated tasks can be scheduled in any relative order.

Example
Kahn's algorithm on a course schedule
mnopqr
One critical path, m to o to p to r (bold), through the prerequisite DAG.

Six courses m,n,o,p,q,rm,n,o,p,q,r have prerequisites m→n,m→o,n→p,o→p,o→q,p→r,q→rm\to n, m\to o, n\to p, o\to p, o\to q, p\to r, q\to r (take the tail before the head). Only mm starts with no prerequisite, so it is first. Removing mm leaves nn and oo free; take nn (breaking the tie alphabetically). Removing nn leaves only oo available (pp still needs oo); take oo. Removing oo leaves pp and qq both free; take pp, then qq (the only ones left besides rr, which still needs qq); finally rr. The order is m,n,o,p,q,rm, n, o, p, q, r — one valid schedule among several.

Example
How many valid schedules are there?

How many topological orders does the prerequisite DAG above have? Only mm has no incoming edge, so mm must be first in every valid order; only rr has no outgoing edge, so rr must be last. The freedom is entirely in how n,o,p,qn, o, p, q interleave, subject to nn before pp, oo before pp, oo before qq, pp before rr, qq before rr. Checking the possibilities directly, exactly 55 orders survive: m,n,o,p,q,rm,n,o,p,q,r; m,n,o,q,p,rm,n,o,q,p,r; m,o,n,p,q,rm,o,n,p,q,r; m,o,n,q,p,rm,o,n,q,p,r; m,o,q,n,p,rm,o,q,n,p,r.

Example
Does this digraph have a topological order?
wxyz
No topological order: x, y, z form a directed cycle (bold).

Does the digraph with edges w→x,x→y,y→z,z→xw\to x, x\to y, y\to z, z\to x have a topological order? Running Kahn's algorithm: ww has in-degree 00 and is removed first, dropping xx's in-degree from 22 to 11. Now every remaining vertex has in-degree at least 11 — xx still needs zz, yy still needs xx, zz still needs yy — so nothing can be removed next, and the algorithm halts with 33 vertices unprocessed. There is no topological order: x→y→z→xx \to y \to z \to x is a directed cycle, and by the theorem above, a digraph with a topological order cannot contain one.

Side note
Where it goes wrong

The DFS visit order (preorder) is NOT a topological order in general — only the REVERSE of the DFS finish order is. Take the digraph a→ca \to c, b→cb \to c, b→db \to d: starting a DFS at aa visits aa then cc (its only neighbor), giving preorder a,c,…a, c, \ldots before bb has been visited at all. But the edge b→cb \to c requires bb before cc, and this preorder puts cc second, ahead of bb — not a valid topological order, even though the graph is a DAG.

Try it
Try it: a topological order

List a topological order for the DAG with edges a→b,a→c,b→d,c→da \to b, a \to c, b \to d, c \to d.

Show the answer

aa has no incoming edges, so it is first; removing it leaves bb and cc both free; finally dd, which needed both. One valid order is a,b,c,d\mathbf{a, b, c, d} (so is a,c,b,da, c, b, d).