This deck covers breadth-first and depth-first search on graphs stored as adjacency lists. It gives a full BFS trace with a queue and shortest-path parent pointers, explains why BFS finds shortest paths only when every edge costs the same, and gives a full DFS trace with discovery and finish times together with the tree, back, forward, and cross edge classification that DFS reveals. It then explains why both run in time proportional to the number of vertices plus edges rather than to the number of vertices squared. It targets four real misconceptions: trusting BFS shortest paths on weighted graphs, forgetting the visited set, assuming quadratic running time, and mixing up which data structure belongs to which traversal.
Subject: CS3000 Algorithms · 140 slides · symbolic lesson
Open the interactive version of this deck · Homework for this lesson
Title
CS3000 Algorithms · Graphs
Two ways to walk every reachable vertex — one level at a time, or as deep as possible before backing out.
Objectives
Every graph algorithm you meet this semester — shortest paths, cycle detection, topological sort, connectivity — is built on top of two basic ways of walking a graph. By the end of this lesson you can:
queue, level by level, and explain why it finds shortest paths only when every edge costs the same.stack, recording discovery and finish times.Concept
Before any new material: cover the screen.
You have named 14 reusable moves so far. Say as many as you can out loud, by number, from memory.
Do not advance until you have actually tried. Getting four of eight is information; skipping the exercise is not.
Here they are. Score yourself.
Today adds no new moves. Every proof in this lesson is built out of the list above. That is the whole point of the list.
The question that starts every proof from here on is not how do I begin. It is which of these applies here?
Section
Section 1
Warm-up
Discussion prompt
Before the formal definition: name three things from everyday life that are really graphs, and say what the vertices and edges are in each.
Hint: Anything where things are connected to other things.
Answer:
Road maps: vertices are intersections, edges are roads. Social networks: vertices are people, edges are friendships. Web pages: vertices are pages, edges are links. Notice the last one is directed and the middle one usually is not.
Concept
A graph is a set of vertices (the dots, also called nodes) connected by edges (the lines between them). A road map, a social network, and a set of course prerequisites are all graphs.
An edge connecting vertex A and vertex B means there is a direct connection between them. A and B are called neighbors when an edge joins them.
graph — A pair consisting of a set of vertices and a set of edges connecting pairs of vertices.
\[ G = (V, E) \]
Picture it
Animation
Shows: A graph is vertices joined by edges — a rendered Manim animation.
Rendered with Manim.
Takeaway: Six vertices, six edges. Nothing else is assumed.
Sorting
Sort into buckets
Decide whether each relationship is naturally symmetric. Your answer determines whether you would model it with a directed or an undirected graph.
Concept
In an undirected graph, an edge between A and B can be crossed in either direction — think of a two-way street.
In a directed graph, an edge points one way only, from a source vertex to a target vertex — think of a one-way street. An edge from A to B does not mean there is also one from B to A.
Both BFS and DFS work on either kind of graph. On a directed graph, neighbor means 'vertex reachable by following an outgoing edge.'
Intuition
Picture a city map. An undirected graph is a city of two-way streets — you can drive from the library to the park and back the same way.
A directed graph is a city with one-way streets. You might drive from the library to the park, but the only way back could be a completely different route — or there might be no way back at all.
Trade off
Comparison matrix
Fill in what each representation costs. This choice decides the running time of everything that follows.
| Adjacency list | Adjacency matrix | |
|---|---|---|
| Space used | V plus E | V squared |
| Check if one specific edge exists | degree of the vertex | constant |
| List all neighbours of a vertex | degree of the vertex | V |
Concept
The adjacency list representation stores, for every vertex, a list of its neighbors. It is the standard way to represent a graph for traversal algorithms.
| Vertex | Neighbors (adjacency list) |
|---|---|
| A | B, C |
| B | A, D |
| C | A, D, E |
Looking up 'who are A's neighbors' costs time proportional to how many neighbors A has — not to the total number of vertices in the graph. That detail is the whole reason traversal is fast.
Prediction
Predict first
Traversal spends its whole life asking one question: who are this vertex's neighbours? Which representation makes that cheap, and what does that do to the total running time?
Correct: The list. You read only real neighbours, so the totals come to V plus E.
Why: The matrix answers the wrong question fast. Traversal never asks whether one specific edge exists; it asks for all neighbours at once, and the matrix makes you scan a whole row of V entries to find them, most of which are absent. That scan is what turns the bound quadratic.
Concept
The degree of a vertex is the number of edges touching it — the length of its adjacency list.
degree — For an undirected graph, the number of neighbors a vertex has.
\[ \sum_{v \in V} \deg(v) = 2|E| \]
We will use this counting fact later to explain exactly why traversal time is proportional to vertices plus edges.
Picture it
Animation
Shows: Why the degrees sum to twice the edges — a rendered Manim animation.
Rendered with Manim.
Takeaway: Every edge is counted exactly twice — once from each end.
Intuition
Picture each vertex as a room, and each edge as a door connecting two rooms. Traversal just means: starting in one room, walk through doors until you have visited every room you can reach.
BFS and DFS are two different strategies for doing that walk. They visit the same reachable rooms in the end — but in a different order, and for different reasons.
Real world
Discussion prompt
You are exploring an unfamiliar building with many rooms and doors, and you want to see every room exactly once. Describe your strategy in plain English, then say what you would have to carry with you to make it work.
Hint: What goes wrong if you keep no record at all?
Answer:
You need two things. A record of rooms already seen, or you will walk in circles forever. And a list of doors noticed but not yet opened, so you can come back to them. Those two are exactly the visited set and the frontier — and the only real difference between BFS and DFS is which end of that frontier you take from.
Concept
A traversal systematically visits every vertex reachable from a starting vertex, following edges, and visits each one exactly once.
'Reachable' means there is some path of edges from the start to that vertex. Revisiting a vertex once it is known would waste work — and on a graph with a cycle, it could cause an infinite loop.
Anomaly
Predict first
A student writes traversal code with a frontier but no visited set. It runs on a triangle graph and never terminates. Where exactly does the loop come from?
Correct: Every vertex keeps being re-added by neighbours that were themselves re-added.
Why: Without a record of what has been seen, visiting a vertex pushes all its neighbours onto the frontier again, including the one you just came from. That neighbour then pushes this vertex back. The frontier never empties, and it has nothing to do with the number of vertices — any graph containing a cycle, or even a single undirected edge, does it.
Concept
Every traversal keeps a visited set — the vertices already discovered. Before following an edge to a vertex, first check whether it is already in the visited set.
visited set — The collection of vertices a traversal has already discovered. Checking it before visiting a vertex again is what prevents infinite loops on graphs with cycles.
Mark a vertex visited the moment you discover it — not when you finish processing it. Otherwise the same vertex can be queued or pushed more than once before anyone notices.
Section
Section 2
Step zero
Discussion prompt
You want to see all rooms one door away before any room two doors away. What discipline must you impose on the frontier to make that happen?
Hint: Think about which waiting item you should serve next.
Answer:
Serve the frontier in the order things arrived — oldest first. Everything one door away is discovered before anything two doors away, so oldest-first means you finish a whole level before starting the next. That discipline is a queue, and the queue is the entire reason BFS finds shortest paths.
Concept
Breadth-first search (BFS) explores a graph outward in layers: first the start vertex, then all of its neighbors, then all of their unvisited neighbors, and so on.
BFS uses a queue — a first-in-first-out structure — to track which vertex to explore next. Vertices are explored in the order they were discovered.
queue — A first-in-first-out (FIFO) structure: whatever was added first is removed first. BFS enqueues newly discovered vertices at the back and dequeues from the front.
Intuition
Drop a stone in a pond. The ripple reaches every point at distance one before it reaches any point at distance two. That is exactly BFS: it finishes an entire layer before moving to the next one.
The queue is what enforces this ordering. New discoveries go to the back of the line, so the traversal always finishes the current layer before starting the next.
Prediction
Predict first
There are two plausible moments to mark a vertex visited: when you first put it on the queue, or when you later take it off. Which is correct, and what breaks with the other?
Correct: When you enqueue it.
Why: If you wait until dequeue, a vertex with three unvisited neighbours pointing at it gets pushed three times before it is ever processed, and it will be expanded three times over. The queue can swell far beyond V and the running time stops being linear, even though the final answer often still looks right.
Concept
BFS from a start vertex, step by step:
Marking a vertex visited the moment it is added to the queue (not when it is later removed) is essential — otherwise the same vertex could be added to the queue more than once.
Concept
A queue, a distance table, and a parent table. The queue is what makes it breadth-first; swap it for a stack and you have written DFS instead.
BFS(G, s)
for each u in V
dist[u] = INFINITY
dist[s] = 0
Q = queue containing s
while Q is not empty
u = Dequeue(Q)
for each v in Adj[u]
if dist[v] == INFINITY
dist[v] = dist[u] + 1
parent[v] = u
Enqueue(Q, v)Line 9 is the visited check, and it does two jobs at once: it stops the algorithm looping forever on a cycle, and it guarantees each vertex is enqueued exactly once, which is what keeps the running time linear.
Notation
Every line of BFS says one thing. Read the line, then read what it does — not the other way round.
Annotate
Invariant
The queue always holds vertices from at most two consecutive layers, with all of the nearer layer ahead of the further one. That ordering is exactly why distances come out right.
Step through it
Before each dequeue, name what is in the queue and at what distance.
Picture it
Animation
Shows: BFS executing: the current line of pseudocode is highlighted while the data it touches changes.
Rendered with Manim.
Takeaway: A queue processes every vertex at distance k before any at distance k plus one, so first discovery is the fewest-hops discovery.
Prediction
Predict first
BFS can tell you the distance to every vertex. To also report the route, what is the least you must store?
Correct: One pointer per vertex, pointing at whoever discovered it.
Why: Storing full paths would cost far more space and duplicate almost everything, since paths share long prefixes. A single parent pointer per vertex is enough: follow the chain backwards from the target to the source and reverse it. Those pointers together form a tree, which is why it is called the shortest-path tree.
Concept
While enqueuing a new vertex, record which vertex discovered it — its parent in the traversal. Following parent pointers backward from any vertex to the start retraces a path.
parent pointer — For a vertex discovered during traversal, the neighbor being processed when it was first found. Chaining parent pointers back to the start reconstructs the path taken to reach it.
Because BFS explores layer by layer, that reconstructed path always turns out to be a shortest path in terms of number of edges — argued on the next few slides.
Picture it
Animation
Shows: The parent pointers form a shortest-path tree — a rendered Manim animation.
Rendered with Manim.
Takeaway: One pointer per vertex is enough to rebuild every route.
Invariant
Step through it
Before each step, say which vertex comes off the queue next. Then check: is the queue ever holding two different distances that differ by more than one?
Worked example
Figure (svg): An undirected graph with six vertices A through F. Edges: A-B, A-C, B-D, C-D, C-E, D-F, E-F.
Run BFS starting at vertex A. Adjacency lists, alphabetical order: A: B, C. B: A, D. C: A, D, E. D: B, C, F. E: C, F. F: D, E.
Initialize
Why: Mark A visited and enqueue it alone. Queue: [A].
Dequeue A, examine its neighbors
Why: A's neighbors are B and C, both unvisited. Mark both visited and enqueue them in that order. Queue becomes [B, C].
Dequeue B, examine its neighbors
Why: B's neighbors are A (visited) and D (unvisited). Enqueue D. Queue becomes [C, D].
Dequeue C, examine its neighbors
Why: C's neighbors are A (visited), D (visited), and E (unvisited). Enqueue E. Queue becomes [D, E].
Dequeue D, examine its neighbors
Why: D's neighbors are B (visited), C (visited), and F (unvisited). Enqueue F. Queue becomes [E, F].
Dequeue E, then F — nothing new
Why: E's neighbors (C, F) are both already visited, and F's neighbors (D, E) are both already visited. The queue empties and BFS ends.
Verify the full trace against the queue and visited-set history
Why: Every vertex was enqueued exactly once and every edge was examined once from each endpoint that processed it, matching the table.
| Step | Dequeued | Neighbors examined | Newly enqueued | Queue after | Visited after |
|---|---|---|---|---|---|
| 1 | (start) | - | A | [A] | {A} |
| 2 | A | B, C | B, C | [B, C] | {A, B, C} |
| 3 | B | A, D | D | [C, D] | {A, B, C, D} |
| 4 | C | A, D, E | E | [D, E] | {A, B, C, D, E} |
| 5 | D | B, C, F | F | [E, F] | {A, B, C, D, E, F} |
| 6 | E | C, F | - | [F] | {A, B, C, D, E, F} |
| 7 | F | D, E | - | [] | {A, B, C, D, E, F} |
Hypothesis
Predict first
From the trace you just stepped through: what stayed true about the queue at every single moment?
Correct: At most two consecutive levels, never more.
Why: This is the invariant the whole correctness proof rests on. While you drain level k you are appending level k plus one behind it, so the queue straddles exactly two levels. It cannot straddle three, because level k plus two is only reachable once level k plus one is being processed, and by then level k is gone.
Concept
Every proof of this kind has the same five or six moves in the same order. The order is not something you rediscover each time.
It is on the right. It will stay on the right through the worked examples that follow.
Why this matters: the structure is now handled. You are not spending working memory on what comes next — you are spending all of it on the one hard step.
Step 1 decides whether this works at all. A claim about all vertices has nothing to induct on; the same claim indexed by distance almost proves itself.
Intuition
What move should we make next?
The claim, for an unweighted graph:
\[ \text{BFS assigns every reachable } v \text{ its true distance } \delta(s, v) \]
Every trace you have run agrees with it. Tracing more graphs will not prove it.
The claim is about all vertices at once, which is not something you can induct on directly. What do you index it by, and which move is that?
_Look at your toolkit. Say a move number out loud before this slide advances._ A wrong guess is useful. A silent guess is not.
Explain it to yourself
Discussion prompt
Suppose BFS assigns some vertex a distance of 5, but there is genuinely a path of length 3 to it. Explain why that is impossible — walk the contradiction out step by step.
Hint: Look at the vertex just before it on that shorter path.
Answer:
If a path of length 3 exists, the vertex just before the target on it sits at distance 2 or less. BFS processes every vertex at distance 2 before any at distance 4 or 5, and when it processed that neighbour it would have discovered the target and given it distance 3. So the target could never still be undiscovered when distance 5 was being handed out. The assumption contradicts the level ordering.
Concept
Claim: BFS from a start vertex discovers vertices in nondecreasing order of their true shortest distance, measured in number of edges, from the start — as long as every edge counts as one step.
Reasoning: the queue always holds vertices from at most two consecutive layers. Every vertex at distance k is enqueued only while processing layer k-1, and the queue's first-in-first-out order guarantees all of layer k-1 is dequeued before any of layer k.
\[ \text{distance}(v) = \text{distance}(\text{parent}(v)) + 1 \]
So the very first time BFS reaches a vertex is guaranteed to be by the shortest possible route in edges — exactly why the parent pointers trace out a shortest path.
Picture it
Animation
Shows: BFS spreads outward, level by level — a rendered Manim animation.
Rendered with Manim.
Takeaway: Everything one hop away is finished before anything two hops away.
Concept
The move: #5 (Peel one off), then #6 (Substitute the hypothesis).
Index the claim by distance, not by vertex
Why: Restate it as: every vertex at true distance k is discovered on layer k. Now there is a number to induct on. This restatement is the whole trick.
Base case: the source
Why: Distance 0, discovered first, assigned 0.
Peel one off: a vertex at distance k+1
Why: Its shortest path has some vertex u at distance k immediately before it. By the hypothesis u is on layer k, and BFS scans every neighbour of u when it dequeues u.
\[ \delta(s, v) = k + 1 \;\Longrightarrow\; v \text{ is discovered while scanning layer } k \]
Notice how the queue does not appear in the argument at all. The queue is the implementation of layer order; the proof is about the layers.
Reverse engineer
Fill in the blanks
The parent pointers say: F came from D, D came from B, B came from A, and A is the source. Complete the reconstruction loop.
path = []
node = target
while node is not source:
path.append(node)
node = parent[node]
path.append(source)
path.reverse()
Why: Following parent pointers walks from the target back towards the source, so the list comes out backwards and must be reversed at the end. The loop stops at the source because the source is the one vertex with no parent — it was never discovered by anybody.
Worked example
Using the parent pointers recorded during the trace on the previous graph, find the shortest path from A to F.
Record each vertex's parent from the trace
Why: B and C were discovered while processing A, so their parent is A. D was discovered while processing B. E was discovered while processing C. F was discovered while processing D.
| Vertex | Parent | Distance |
|---|---|---|
| A | - | 0 |
| B | A | 1 |
| C | A | 1 |
| D | B | 2 |
| E | C | 2 |
| F | D | 3 |
Walk parent pointers backward from F
Why: F's parent is D, D's parent is B, B's parent is A. Reading that chain in reverse gives the path from A to F.
\[ A \to B \to D \to F \]
Verify the path length matches F's recorded distance
Why: The path A to B to D to F has 3 edges, matching F's distance of 3. No shorter path exists, since BFS discovers vertices in nondecreasing distance order and F was first reached at distance 3.
\[ |A \to B \to D \to F| = 3 = \text{distance}(F)\ \checkmark \]
Pattern
Step through it
Before each step: which vertex does the pointer send you to next? Notice the path comes out backwards.
Intuition
What feels wrong about this?
A graph with two routes from s to t: one edge of weight 10, or two edges of weight 1 each.
\[ s \to t \; (\text{cost } 10) \qquad \text{versus} \qquad s \to u \to t \; (\text{cost } 2) \]
BFS reports the one-edge route, because it is one hop.
_Plain English only. No notation, no algebra. Just say what bothers you._
The feeling: BFS is counting hops, and nobody told it that the hops have different prices.
That feeling is the proof. It is not a substitute for the proof — it is the thing the proof writes down.
So the fix is not to patch BFS — it is to notice that the proof used distance equals number of edges in the induction step. Change what distance means and the induction step is simply false. That is Dijkstra's whole reason to exist, two lessons from now.
Picture it
Animation
Shows: Where BFS gives the wrong answer — a rendered Manim animation.
Rendered with Manim.
Takeaway: BFS is not wrong — it is answering a different question.
Counterexample
Discussion prompt
Claim: BFS always finds the cheapest route. Draw a small weighted graph where BFS returns a route that is genuinely more expensive than another available one.
Hint: Two vertices, two routes: one hop that is expensive, or several hops that are each cheap.
Answer:
Take A to D directly with weight 100, and also A to B to C to D with each edge weight 1. BFS counts hops, so it reports the one-hop route as best, at total cost 100. The three-hop route costs 3. BFS is not wrong about anything it claims — it minimises the number of edges — it is simply answering a different question from the one weights ask.
Trap
A student runs BFS on a graph whose edges have different costs (a weighted graph) and assumes the BFS tree still gives the cheapest paths.
| Edge | Weight |
|---|---|
| S - T | 10 |
| S - A | 1 |
| A - T | 1 |
Treat 'fewest edges' as 'cheapest'
Why: BFS from S reaches T directly in one hop (the edge S-T), so a student concludes the shortest path from S to T is that direct edge, at total cost 10.
\[ \text{BFS conclusion: } S \to T,\ 1 \text{ hop, cost } 10 \]
BFS only counts edges, never their weights. On a weighted graph you must compare total weight, not hop count.
| Edge | Weight |
|---|---|
| S - T | 10 |
| S - A | 1 |
| A - T | 1 |
Compare total weight of every path
Why: The direct edge S-T costs 10. The two-hop path S-A-T costs 1+1 = 2, which is cheaper despite using more edges.
\[ S \to A \to T:\ 1+1=2 \ < \ 10 \]
Use the right tool for weighted graphs
Why: BFS's shortest-path guarantee holds only when every edge has the same cost — an unweighted (or unit-weight) graph. For graphs with different edge weights, use an algorithm built for weights, such as Dijkstra's algorithm.
Error analysis
Annotate
This BFS terminates and looks reasonable. It reports the wrong distances. Find the line that does it.
Trap
On a 3-vertex cycle, a student writes BFS without ever checking whether a vertex has already been visited.
\[ A - B, \quad B - C, \quad C - A \]
Enqueue every neighbor, visited or not
Why: Starting from A: enqueue B and C. Dequeuing B re-enqueues A and C, since nothing marks them as already handled. Dequeuing C does the same.
| Dequeued | Queue after (no visited check) |
|---|---|
| A | [B, C] |
| B | [C, A, C] |
| C | [A, C, A, B] |
The queue keeps growing forever
Why: Every vertex keeps re-adding its neighbors because nothing is ever recognized as 'already seen.' The queue never empties, and the loop never terminates.
Mark each vertex visited the instant it is enqueued, and never enqueue a vertex already in the visited set.
\[ A - B, \quad B - C, \quad C - A \]
Check the visited set before enqueuing
Why: Starting from A: mark A visited, enqueue it. Dequeuing A finds B and C unvisited, so mark and enqueue both.
| Dequeued | Neighbors checked | Newly enqueued | Visited after |
|---|---|---|---|
| (start) | - | A | {A} |
| A | B, C | B, C | {A, B, C} |
| B | A, C | - | {A, B, C} |
| C | A, B | - | {A, B, C} |
The traversal terminates correctly
Why: Once B and C are dequeued, every one of their neighbors is already visited, so nothing new is enqueued. The queue empties after exactly 3 dequeues — one per vertex.
Missing information
Discussion prompt
You run BFS from vertex A on a six-vertex graph and it reports distances for only four vertices. No bug is present. What does that tell you about the graph, and what must you change to visit everything?
Answer:
The graph is disconnected — the other two vertices are in a component with no route from A at all. A single BFS explores exactly one component by design. To cover the whole graph, loop over all vertices and start a fresh BFS from any that is still unvisited. The number of times you have to start is the number of connected components.
Concept
Not every graph is fully connected. If some vertices cannot be reached from the start at all, a single BFS run never visits them — the queue empties while vertices remain unvisited.
To visit every vertex in the whole graph, run BFS again from any unvisited vertex, repeating until none remain. Each run discovers one connected component.
connected component — A maximal set of vertices that can all reach each other. Running a full traversal from every yet-unvisited vertex, one component at a time, discovers all of them.
Worked example
Graph: vertices A, B, C, D with edges A-B and C-D only. Run BFS, restarting whenever vertices remain unvisited.
\[ A - B, \quad C - D \]
Run BFS from A
Why: A's only neighbor is B. Enqueue and visit both. B's only neighbor, A, is already visited, so this run ends with {A, B} visited and C, D untouched.
| Run | Start | Visits in this run |
|---|---|---|
| 1 | A | A, B |
C is unvisited, so start a new run from C
Why: C's only neighbor is D. Enqueue and visit both. This second run discovers the second component, {C, D}.
| Run | Start | Visits in this run |
|---|---|---|
| 1 | A | A, B |
| 2 | C | C, D |
Verify every vertex was visited exactly once
Why: Together the two runs visited A, B, C, D — all four vertices, each exactly once — revealing two separate connected components: {A, B} and {C, D}.
Invariant
Step through it
Watch which vertices are reachable at each stage. What has to happen before the second group is ever touched?
Ranking
Put in order
Arrange these into the correct BFS loop body order.
Why: Marking must happen before enqueueing, and both must sit behind the visited check. Getting this order wrong is what produces the duplicate-enqueue bug that silently destroys the linear running time while often leaving the distances correct.
Pattern
1. Mark the start vertex visited and enqueue it alone
Why: The queue always begins with exactly one vertex — the source.
2. Dequeue the front vertex and look at its neighbors
Why: Processing the front of the queue guarantees the current layer finishes before the next one begins.
3. Mark each unvisited neighbor visited and enqueue it, recording its parent
Why: Marking on discovery (not on dequeue) is what prevents duplicate enqueues. Recording the parent builds the shortest-path tree.
4. Repeat until the queue is empty; restart from any unvisited vertex if needed
Why: An empty queue means the current component is fully explored. Restarting reaches every remaining component.
Picture it
Animation
Shows: The invariant the whole proof rests on — a rendered Manim animation.
Rendered with Manim.
Takeaway: A third level cannot appear until level k is already gone.
Scale up
Step through it
Watch the queue size as the branching factor grows. Predict the peak before stepping.
Check
Run BFS starting at vertex A on the graph below (same graph as the earlier trace).
Figure (svg): The same undirected six-vertex graph A through F used in the BFS trace.
Check your understanding
After A is dequeued and its neighbors B and C are enqueued, B is dequeued next. B's only unvisited neighbor is D. What is the queue immediately after processing B?
Answer: A
Why: Before processing B the queue was [B, C] with B at the front. Dequeuing B removes it from the front, leaving [C]. B's unvisited neighbor D is then enqueued at the back, giving [C, D].
Section
Section 3
Prediction
Predict first
Take working BFS and change only the frontier discipline from take-oldest to take-newest. What does the algorithm become?
Correct: Depth-first search.
Why: The two algorithms are the same program with a different container. Oldest-first is a queue and spreads outward level by level; newest-first is a stack and plunges along one path until it dead-ends. Everything else — the visited set, the neighbour loop, the V plus E bound — is untouched.
Concept
Depth-first search (DFS) explores as far as possible along one path before backing up. From the current vertex it plunges into an unvisited neighbor immediately, rather than finishing all neighbors first.
DFS can be written recursively — visit a vertex, then recursively visit each unvisited neighbor — or iteratively with an explicit stack. Both produce the same style of traversal: deep, then back out.
stack — A last-in-first-out (LIFO) structure: whatever was added most recently is removed first. DFS's recursive call stack behaves exactly like an explicit stack of 'vertices left to finish.'
Analogy
Match the pairs
Match the search strategy to the human behaviour it mimics.
Why: The behaviours differ only in what you do when you notice several unexplored doors at once. BFS notes them all and comes back in the order noticed; DFS walks straight through the most recent one and postpones the rest until it is forced to back up.
Intuition
Imagine exploring a maze with a ball of string. You commit to one hallway and follow it as far as it goes, only turning back when you hit a dead end or a room already visited.
That backing-up is called backtracking. Each retreat returns to the most recent junction with an unexplored hallway — exactly what a stack (or recursion) remembers for you automatically.
Notation
Annotate
DFS stamps every vertex twice. Take the notation apart before using it.
Concept
DFS keeps a running clock. Each vertex gets a discovery time — when it is first visited — and a finish time — when its recursive call, having explored everything reachable through it, returns.
discovery time — The tick of DFS's clock at the moment a vertex is first reached and marked visited.
finish time — The tick of DFS's clock at the moment all of a vertex's neighbors have been fully explored and its recursive call is about to return.
Every vertex's interval from discovery to finish is either completely nested inside another vertex's interval, or completely separate from it — never partially overlapping. That fact is what lets discovery/finish times classify every edge.
\[ [\,d[u],\, f[u]\,] \text{ and } [\,d[v],\, f[v]\,] \text{ are nested or disjoint} \]
Concept
The same traversal with the queue replaced by recursion, plus two clocks. Those clocks are what turn DFS from a way of visiting vertices into a tool that detects cycles and orders a DAG.
DFS-VISIT(G, u)
time = time + 1
d[u] = time
color[u] = GREY
for each v in Adj[u]
if color[v] == WHITE
parent[v] = u
DFS-VISIT(G, v)
else if color[v] == GREY
report a back edge, so G has a cycle
color[u] = BLACK
time = time + 1
f[u] = timeGrey means 'entered but not finished' — still on the recursion stack. So an edge into a grey vertex points back to one of your own ancestors, and an ancestor plus the path down to you is a cycle. That is the whole cycle test.
Notation
Every line of DFS says one thing. Read the line, then read what it does — not the other way round.
Annotate
Invariant
The grey vertices, at any moment, are precisely the path from the root down to where you are standing. That is why an edge into a grey vertex always closes a cycle.
Step through it
At each step, list the grey vertices and check they form a single path.
Picture it
Animation
Shows: DFS executing: the current line of pseudocode is highlighted while the data it touches changes.
Rendered with Manim.
Takeaway: Grey means still on the stack, so an edge into a grey vertex is a back edge — and a back edge is a cycle.
Definition probe
Sort into buckets
You are standing at vertex u and looking down an edge at vertex v. Sort each observation by what colour v must be.
Concept
DFS tracks each vertex's status with a color. White means not yet discovered. Gray means discovered but not yet finished — still open on the current path. Black means finished — fully explored.
A vertex is gray for the entire stretch between its discovery time and its finish time. That is exactly the set of vertices currently on the path from the start down to where DFS is right now.
Intuition
White is a light that has not turned on yet — untouched. Gray is a yellow light — in progress, still active, still on your current route. Black is a light already passed — fully handled, you will not return to it.
When DFS looks at an edge to a gray vertex, it has found a way back to somewhere still 'in progress' on the current path — that is exactly the signature of a cycle.
Invariant
Step through it
Before each step, predict which vertex turns gray next. Then watch the gray vertices: do they ever fail to form a single unbroken path?
Worked example
Figure (svg): A directed graph with six vertices. Tree edges A to B, A to C, B to D, C to F, D to E form the DFS tree. A dashed forward edge runs from A to D, a dashed cross edge runs from C to D, and a curved dashed back edge runs from E up to B.
Run DFS starting at A on the directed graph below. Adjacency lists in visiting order: A: B, C, D. B: D. C: D, F. D: E. E: B. F: none.
Discover A
Why: Clock starts at 1. Mark A gray and set its discovery time. A's first unvisited neighbor is B.
\[ d[A]=1 \]
Discover B, then D, then E
Why: Following the first unvisited neighbor each time: A leads to B, B leads to D, D leads to E.
\[ d[B]=2,\ d[D]=3,\ d[E]=4 \]
E has no unvisited neighbors — finish E, then D, then B
Why: E's only neighbor B is already gray, so nothing new to explore from E. E finishes. D's only neighbor was E, now finished, so D finishes next. B's only neighbor was D, now finished, so B finishes last of the three.
\[ f[E]=5,\ f[D]=6,\ f[B]=7 \]
Back at A, discover C, then F
Why: A's next neighbor is C. C's first unvisited neighbor is F; F has no neighbors, so it finishes immediately.
\[ d[C]=8,\ d[F]=9,\ f[F]=10 \]
C has no more unvisited neighbors — finish C, then finish A
Why: C finishes once F is done. A has now explored all three neighbors (B, C, D — D was already visited), so A finishes last.
\[ f[C]=11,\ f[A]=12 \]
Verify every interval nests or is disjoint as required
Why: B's interval [2,7] contains D's [3,6], which contains E's [4,5] — properly nested, since B, D, E lie on one path. C's interval [8,11] contains F's [9,10] and is entirely disjoint from D's [3,6] and B's [2,7], since C's subtree was explored after they finished.
| Vertex | Discovery | Finish |
|---|---|---|
| A | 1 | 12 |
| B | 2 | 7 |
| C | 8 | 11 |
| D | 3 | 6 |
| E | 4 | 5 |
| F | 9 | 10 |
Picture it
Animation
Shows: Two stamps per vertex — a rendered Manim animation.
Rendered with Manim.
Takeaway: Gray means still open — still somewhere below you on the path.
Translation
Match the pairs
Match each piece of the recursive DFS to what it becomes when you write the loop with an explicit stack.
Why: Recursion is not a different algorithm, it is a stack the language manages for you. Writing the stack yourself makes that explicit and is what saves you when the graph is deep enough to blow the call stack.
Concept
Recursive DFS on a vertex does three things: mark it visited, then recursively call itself on each unvisited neighbor, then return (finish) once all neighbors are handled.
An explicit-stack version does the same job without function calls: push a vertex, and when it is popped, mark it visited and push its unvisited neighbors. The stack's top always holds the most recently discovered, not-yet-finished vertex — exactly what the recursive call stack tracks automatically.
Worked example
Trace an explicit-stack DFS on the same 6-vertex graph from the BFS example, starting at A. Mark a vertex visited the moment it is pushed, so it is never pushed twice.
Push A, then pop and process it
Why: Stack starts as [A]. Popping A marks it visited (order 1) and pushes its unvisited neighbors B and C, in reverse order so B ends on top: stack becomes [C, B].
Pop B, then D
Why: B is visited (order 2); its only unvisited neighbor is D, pushed, giving [C, D]. Popping D visits it (order 3) and pushes its unvisited neighbor F, giving [C, F].
Pop F, then E
Why: F is visited (order 4); its only unvisited neighbor is E, pushed, giving [C, E]. Popping E visits it (order 5); both of E's neighbors (C, F) are already visited, so nothing is pushed, leaving [C].
Pop C — traversal ends
Why: C is visited (order 6); all of its neighbors (A, D, E) are already visited, so nothing is pushed. The stack is now empty and every vertex has been visited.
Verify the discovery order differs from BFS on the same graph
Why: Stack-based DFS visited A, B, D, F, E, C — plunging down one path before backing out — while BFS on the identical graph visited A, B, C, D, E, F in level order. Same graph, same start, genuinely different orders, because a queue and a stack hand back vertices in opposite orders.
| Order | BFS (queue) | DFS (stack) |
|---|---|---|
| 1 | A | A |
| 2 | B | B |
| 3 | C | D |
| 4 | D | F |
| 5 | E | E |
| 6 | F | C |
Invariant
Step through it
Compare each frame to the recursive trace you just saw. What is on the stack at every moment?
Two truths and a lie
Sort into buckets
Sort each claim by whether it is actually true.
Trap
A student wants to run BFS but implements the frontier with a stack instead of a queue — popping the most recently added vertex instead of the oldest.
| Order | Intended (BFS, queue) | Actual (used a stack) |
|---|---|---|
| 1 | A | A |
| 2 | B | B |
| 3 | C | D |
Use last-in-first-out order by mistake
Why: With a stack, the vertex added most recently (D, discovered while processing B) is explored before C, which was discovered earlier while processing A. The traversal plunges deep instead of spreading level by level, and it stops being BFS at all.
BFS needs first-in-first-out order — a queue — so the entire current layer finishes before the next layer starts.
| Order | BFS (queue) |
|---|---|
| 1 | A |
| 2 | B |
| 3 | C |
Use a queue for BFS, a stack or recursion for DFS
Why: A queue processes discoveries in the order they arrived, preserving layers and the shortest-path guarantee. A stack processes the newest discovery first, which is what makes DFS plunge deep — correct for DFS, wrong for BFS.
Match the structure to the guarantee you need
Why: Need shortest paths by edge count? Use a queue (BFS). Need to explore deeply, find cycles, or compute discovery/finish times? Use a stack or recursion (DFS). Swapping them changes both the order and the guarantees.
Fill the middle
Fill in the blanks
The first and last lines are given. Supply the middle.
def visit(u):
color[u] = "gray"
d[u] = next_time()
for v in adj[u]:
if color[v] == "white":
parent[v] = u
visit(v)
color[u] = "black"
f[u] = next_time()
Why: The vertex turns gray on entry and black only after the loop, which is exactly what makes the gray set equal to the current path. Recursing only into white neighbours is what keeps each vertex visited once and preserves the linear bound.
Pattern
1. Mark the start vertex visited (discovered) and open it
Why: Recursively, this is entering the function call; iteratively, this is popping it from the stack.
2. Visit its first unvisited neighbor and recurse (or push it)
Why: Go as deep as possible before considering any sibling neighbor.
3. When no unvisited neighbors remain, mark the vertex finished and back out
Why: This return step produces the finish time and lets the previous call continue with its next neighbor.
4. Repeat from any unvisited vertex until none remain
Why: Just like BFS, one call only reaches one connected component; restart to cover the rest of the graph.
Picture it
Animation
Shows: The gray vertices are the call stack — a rendered Manim animation.
Rendered with Manim.
Takeaway: Never a scattered set — always one unbroken chain.
Cost model
Annotate
Point at the lines that dominate the running time — not the ones that look busiest.
Check
In the DFS trace above, vertex B has discovery/finish times (2, 7) and vertex D has discovery/finish times (3, 6).
Check your understanding
What does this pair of intervals tell you about the relationship between B and D?
Answer: A
Why: D's interval [3,6] falls entirely inside B's interval [2,7]: 2 is less than 3, and 6 is less than 7. Nested intervals mean D was discovered and finished while B was still open — the signature of D being a descendant of B in the DFS tree.
Section
Section 4
Step zero
Discussion prompt
Before the four names: you run DFS and it builds a tree. Every edge of the graph either is in that tree or is not. For the ones that are not, list every structurally different place the other endpoint could sit.
Hint: Above you, below you, or off to the side entirely.
Answer:
Either it points back up to an ancestor, or down to a descendant it did not discover, or across to a vertex in a completely separate part of the tree. Those three plus the tree edges themselves are the only possibilities — which is why there are exactly four categories and not five.
Concept
A tree edge is an edge DFS actually uses to discover a new vertex — followed when moving from a gray vertex to a white, undiscovered neighbor.
Tree edges, taken together, form the DFS tree (or forest, for a graph with multiple components) — the skeleton of parent-to-child discoveries the traversal built.
Picture it
Animation
Shows: Direction changes what is reachable — a rendered Manim animation.
Rendered with Manim.
Takeaway: Following arrows only, some vertices may not be reachable at all.
Prediction
Predict first
Only one of the four edge types is direct evidence of a cycle. Which, and why does it prove it?
Correct: A back edge.
Why: A back edge points at a gray vertex, meaning that vertex is an ancestor still open on the current path. The tree path from that ancestor down to where you stand, plus this one edge closing back up to it, is a cycle you can point at explicitly. Cross and forward edges point into finished territory, so no route leads back and no cycle is formed.
Concept
A back edge connects a vertex to one of its own ancestors — a vertex still gray, still open on the current path, when the edge is examined.
Finding a back edge means DFS has looped back to somewhere still in progress on the current path. That is only possible if the graph contains a cycle.
\[ \text{back edge } (u,v):\quad d[v] < d[u] < f[u] < f[v],\ v \text{ still gray} \]
Intuition
Picture the DFS tree as a family tree, drawn as you meet people while walking down generations. A tree edge introduces a new child. A back edge is like meeting your own grandparent again, further down the line — a loop to someone still 'above' you on the same branch.
Impossible in a real family tree, but very possible in a graph — and whenever it happens, it exposes a cycle running through the ancestor and back down to the current vertex.
Discrimination
Sort into buckets
For each description of the edge from u to v, name the category.
Concept
A forward edge connects a vertex to one of its own descendants — a vertex already fully finished (black) by the time the edge is examined, but whose interval nests inside the current vertex's still-open interval.
\[ \text{forward edge } (u,v):\quad d[u] < d[v] < f[v] < f[u] \]
A forward edge is a bit like a shortcut: it points from an ancestor directly down to a descendant, skipping past the ones already found through the actual tree edges.
Concept
A cross edge connects two vertices where neither is an ancestor of the other — their discovery/finish intervals are completely disjoint, one ending entirely before the other begins.
\[ \text{cross edge } (u,v):\quad f[v] < d[u] \ \text{or}\ f[u] < d[v] \]
Cross edges often point 'sideways,' or backward in time, to an already-finished subtree explored earlier in the traversal — a completely separate branch, or an earlier branch of the same tree.
Intuition
Watch me not know the answer. This is what the first two minutes actually look like.
Given a DFS run, classify each edge as tree, back, forward or cross.
Try classifying by direction on the drawing: up the tree is a back edge, down is forward, sideways is cross
Why: The picture makes the categories look obvious, and for the first few edges it works.
The drawing lies as soon as the tree gets deep
Why: Which node looks above which depends on how you laid the picture out, and cross edges between different subtrees are impossible to distinguish from forward edges by eye.
Dead end. Not a mistake — a move that was worth trying and did not pay off. This happens in most proofs.
Back up. Use the numbers, not the picture
Why: Discovery and finish times give a nesting structure: one interval is inside another, or the two are disjoint. That is a fact about the run, independent of any drawing.
\[ d[u] < d[v] < f[v] < f[u] \;\Longrightarrow\; v \text{ is a descendant of } u \]
Replacing a visual judgment with a numeric test is a recurring move in this course. The picture generates the intuition; the numbers carry the proof.
The expert does not see the whole path in advance. The expert tries something, reads the result, and adjusts. That is the skill.
Comparison
Comparison matrix
Complete the table. The colour column is the one you can actually check while the algorithm runs.
| Edge type | Colour of v when seen | Signals a cycle? |
|---|---|---|
| Tree | white | no |
| Back | gray | yes |
| Forward | black | no |
| Cross | black | no |
Concept
Every edge DFS examines falls into exactly one of the four categories, decided by comparing intervals at the moment the edge is examined.
| Edge type | What you see when you examine it |
|---|---|
| Tree | v is white (undiscovered) — you recurse into it right now. |
| Back | v is gray — v is a currently open ancestor. |
| Forward | v is black, and v's interval nests inside u's still-open interval. |
| Cross | v is black, and v's interval is disjoint from u's (finished before u started). |
Sorting
Sort into buckets
DFS on a directed graph produced the tree A to B to C, and separately A to D. Classify each remaining edge.
Worked example
Using the discovery/finish times from the earlier trace, classify each of the 8 directed edges.
| Vertex | Discovery | Finish |
|---|---|---|
| A | 1 | 12 |
| B | 2 | 7 |
| C | 8 | 11 |
| D | 3 | 6 |
| E | 4 | 5 |
| F | 9 | 10 |
Classify the tree edges
Why: A to B, A to C, B to D, C to F, and D to E were each followed while the target vertex was still white — exactly the 5 tree edges.
Classify E to B
Why: When E examines this edge, B has interval [2,7] and E's discovery time (4) falls inside it while B is still open — B is an ancestor of E through B to D to E. So E to B is a back edge.
\[ d[B]=2 < d[E]=4 < f[E]=5 < f[B]=7 \]
Classify A to D
Why: When A examines this edge, D already has both a discovery and finish time, and D's whole interval sits inside A's still-open interval. A is an ancestor of D through A to B to D, so this is a forward edge.
\[ d[A]=1 < d[D]=3 < f[D]=6 < f[A]=12 \]
Classify C to D
Why: When C examines this edge, C is open at time 8, but D already finished at time 6 — entirely before C even started. The intervals [3,6] and [8,11] are disjoint, so neither is an ancestor of the other: a cross edge.
\[ f[D]=6 < d[C]=8 \]
Verify all 8 edges are accounted for
Why: Five tree edges (A-B, A-C, B-D, C-F, D-E) plus one back edge (E-B), one forward edge (A-D), and one cross edge (C-D) totals eight — matching every edge in the graph, with none left unclassified.
| Edge | Classification |
|---|---|
| A to B | Tree |
| A to C | Tree |
| A to D | Forward |
| B to D | Tree |
| C to D | Cross |
| C to F | Tree |
| D to E | Tree |
| E to B | Back |
Picture it
Animation
Shows: DFS plunges, then backtracks — a rendered Manim animation.
Rendered with Manim.
Takeaway: Same graph, same start, completely different order.
Pattern
Step through it
Before each step, predict the colour of the far endpoint. The colour alone decides the classification.
Intuition
What move should we make next?
The claim to prove, in a directed graph:
\[ \text{DFS finds a back edge} \iff \text{the graph has a cycle} \]
Two directions to prove, and they are not equally easy.
Which direction is nearly free, and which one needs a move? Name the move for the hard direction.
_Look at your toolkit. Say a move number out loud before this slide advances._ A wrong guess is useful. A silent guess is not.
Hypothesis
Predict first
You run DFS over an entire directed graph and find no back edges anywhere. What have you just proved?
Correct: The graph has no cycles at all. It is a directed acyclic graph.
Why: Back edges and cycles imply each other. Any cycle forces DFS to eventually look at an edge pointing back to a vertex still gray on the current path, so no back edges means no cycles. This is not merely a test that usually works — it is the standard way cycle detection is actually implemented, and it costs nothing beyond the traversal you were doing anyway.
Worked example
Claim: the graph from the DFS trace contains a directed cycle, and the back edge found during DFS reveals exactly which vertices form it.
Recall the back edge
Why: DFS found that E to B is a back edge — B is still gray, an open ancestor, when E examines this edge.
\[ \text{back edge: } E \to B \]
Trace the tree path from B down to E
Why: The tree edges show B to D, then D to E — so B reaches E by following two tree edges.
\[ B \to D \to E \ \text{(tree edges)} \]
Combine the tree path with the back edge
Why: Following the tree path from B to E, then taking the back edge from E straight to B, traces a closed loop.
\[ B \to D \to E \to B \]
Verify this is genuinely a cycle
Why: Every edge in the loop, B to D, D to E, and E to B, is a real edge in the graph, and the loop starts and ends at the same vertex B. A back edge during DFS on a directed graph always signals a cycle exactly like this one.
\[ B \to D \to E \to B\ \checkmark \]
Picture it
Animation
Shows: A back edge is a cycle you can point at — a rendered Manim animation.
Rendered with Manim.
Takeaway: A to B to C, then C points back at a still-open A. That is the cycle.
Invariant
Step through it
Track which vertices are gray. At the moment the cycle is found, what exactly do the gray vertices form?
Counterexample
Discussion prompt
In an undirected graph, DFS only ever produces tree edges and back edges. Try to construct a cross edge, and explain what stops you.
Hint: An undirected edge is examined from both ends.
Answer:
You cannot. A cross edge would need v finished and unrelated to u when the edge from u is examined. But an undirected edge is visible from both ends, so while v was being explored it would have seen u — and u was white or gray then, so v would have taken the edge itself and made it a tree or back edge. The edge gets claimed before a cross classification is ever possible.
Concept
In an undirected graph, DFS can only ever produce tree edges and back edges — never forward or cross edges.
The reason: every edge is examined from both ends. If the edge would have been a forward edge from one end, it is discovered as the matching back edge from the other end first, since that end is visited earlier — so it always ends up classified as tree or back.
Concept
These traversals are not the end goal — they are the engine underneath much of the rest of the course. Cycle detection just checks whether DFS ever finds a back edge.
Ordering tasks with prerequisites, a topological sort, works by listing vertices in decreasing order of DFS finish time. Finding connected components, testing bipartiteness, and computing shortest paths in unweighted graphs all start from exactly the BFS or DFS you just traced by hand.
Elimination
Eliminate the wrong options
You are inside the neighbour loop and want to decide, right now, whether this edge is a back edge. Which test works?
Survives elimination: A
Why: Gray means discovered but not finished, which is exactly the definition of an ancestor still on the current path. It is a single constant-time colour lookup available at the precise moment you need it, which is why cycle detection adds nothing to the running time.
Pattern
1. Note the color of the target vertex the moment the edge is examined
Why: White means tree edge — you are about to recurse into it right now.
2. If the target is gray, it is a back edge
Why: Gray means the target is a currently open ancestor on the same path — a loop back, revealing a cycle.
3. If the target is black, compare the two discovery/finish intervals
Why: If the target's interval nests inside the source's still-open interval, it is a forward edge. If the two intervals are disjoint, it is a cross edge.
Picture it
Animation
Shows: The colour decides the classification — a rendered Manim animation.
Rendered with Manim.
Takeaway: One constant-time colour lookup, available exactly when you need it.
Check
Vertex C has discovery/finish times (8, 11) and vertex D has discovery/finish times (3, 6). DFS examines the edge C to D.
Check your understanding
How should the edge C to D be classified?
Answer: A
Why: D's finish time (6) is less than C's discovery time (8), so D's whole interval ends before C's interval even begins. The intervals are disjoint, meaning neither vertex is an ancestor of the other — exactly the definition of a cross edge.
Section
Section 5
Warm-up
Discussion prompt
Before the derivation: the traversal bound is written as V plus E rather than as a single letter. Why two terms instead of one?
Answer:
Because two independent things are being counted, and neither controls the other. You touch each vertex once for its own bookkeeping, and you look at each edge once while scanning neighbour lists. A graph can be huge in vertices and tiny in edges or the reverse, so neither term can be dropped.
Concept
To describe running time, we describe how the number of basic steps grows as the input grows, ignoring constant factors and lower-order details.
\[ O(\cdot)\ \text{gives an upper bound}; \quad \Theta(\cdot)\ \text{gives a tight bound (upper and lower)} \]
For BFS and DFS, the input size has two parts: the number of vertices and the number of edges. We will show the running time is proportional to their sum.
\[ \Theta(|V| + |E|) \]
Estimation
Predict first
An undirected graph has 10 vertices and 15 edges. Across the whole traversal, how many times in total is a neighbour-list entry examined?
Correct: 30 — each undirected edge appears in two lists, so twice the edge count.
Why: An undirected edge is stored in both endpoints' lists, so summing the degrees over all vertices gives exactly twice the number of edges. That sum is the total number of neighbour inspections, and it is why the bound is linear in the edge count rather than in the square of the vertex count.
Concept
Both BFS and DFS, when a vertex is processed, look at exactly its own adjacency list — nothing more. Checking a vertex's neighbors costs time proportional to that vertex's degree, not to the total vertex count.
That is the entire reason these traversals are fast: no vertex ever scans the whole graph looking for its neighbors. It only reads the list it already has.
Picture it
Animation
Shows: The adjacency list stores only real edges — a rendered Manim animation.
Rendered with Manim.
Takeaway: Reading every list once costs 2E, not V squared.
Intuition
Every vertex is processed exactly once — dequeued once in BFS, or discovered-and-finished once in DFS. That is where the vertex term comes from.
Every edge is examined a small, constant number of times total across the whole run — once from each endpoint that processes it. That is where the edge term comes from. Add those two costs together and the total work is proportional to vertices plus edges, never to vertices squared.
Picture it
Animation
Shows: Why nested loops are not quadratic here — a rendered Manim animation.
Rendered with Manim.
Takeaway: Ask what the inner work TOTALS, not how deep the nesting goes.
Scale up
Parameter explorer
Drag V and watch how a matrix-based traversal grows against a list-based one on a sparse graph. Where do they visibly part company?
\[ \text{matrix work} \approx {V}^2 \]
Concept
Adding up the length of every vertex's adjacency list counts each edge exactly twice in an undirected graph — once from each endpoint.
\[ \sum_{v \in V} \deg(v) = 2|E| \quad (\text{undirected}) \]
So the total number of neighbor-checks across the entire traversal is exactly twice the edge count for an undirected graph — a fixed multiple of the edge count, not something that grows with the number of vertices on its own.
Edge cases
Discussion prompt
Two extremes. First, a graph with V vertices and no edges at all. Second, a complete graph where every vertex joins every other. What does the V plus E bound become in each, and what does that tell you about which term matters?
Hint: How many edges does a complete graph have?
Answer:
With no edges the bound is just V — you still touch every vertex, which is why the V term cannot be dropped. In a complete graph the edge count is about V squared over two, so the bound becomes quadratic and the E term completely dominates. The same linear formula covers both because it is linear in the size of the input, not in the number of vertices.
Worked example
Count the actual operations performed in the earlier 6-vertex BFS trace and compare the total to vertices-squared.
| Vertex | Degree (adjacency list length) |
|---|---|
| A | 2 |
| B | 2 |
| C | 3 |
| D | 3 |
| E | 2 |
| F | 2 |
Count the dequeue operations
Why: Each of the 6 vertices is dequeued exactly once over the whole trace.
\[ 6 \text{ dequeues} \]
Count the neighbor-checks
Why: Summing the degrees: 2+2+3+3+2+2 gives 14 neighbor-checks total, matching twice the edge count.
\[ 2+2+3+3+2+2 = 14 = 2|E| \]
Verify the total is proportional to vertices plus edges, not vertices squared
Why: Total operations: 6 dequeues plus 14 neighbor-checks equals 20 — close to vertices plus edges (6+7=13), within a small constant factor. Compare to vertices squared: 6 squared is 36, well above the actual work, and the gap only grows on bigger, sparser graphs.
\[ 6 + 14 = 20 \ \text{vs.} \ |V|^2 = 36\ \checkmark \]
Scale up
Step through it
Two counters run at once. Predict which one grows faster on a graph with many edges.
Concept
DFS follows the identical accounting: every vertex is discovered and finished exactly once, and every edge is examined exactly once from the vertex that processes it.
So DFS's running time is also proportional to vertices plus edges — the same bound as BFS, for the same underlying reason: with adjacency lists, no vertex ever does more work than looking at its own neighbors.
\[ T_{\text{DFS}} = \Theta(|V| + |E|) \]
Anomaly
Predict first
Traversal has a loop over vertices with a loop over neighbours inside it. Nested loops usually multiply. Why is this one not V squared?
Correct: The inner loop lengths add up to E instead of multiplying by V.
Why: Multiplying only applies when the inner loop runs the same number of times regardless of the outer variable. Here it runs degree-of-u times, and those degrees sum to twice the edge count over the whole run. The right question about a nested loop is never how deep it nests, but what the inner work totals across all outer iterations.
Trap
A student reasons: for each vertex, we might have to check it against every other vertex to see if it is a neighbor, so the running time must be proportional to vertices squared.
\[ \text{Assumed: } T = \Theta(|V|^2) \]
Ignore the adjacency-list representation
Why: This reasoning assumes checking whether one vertex is a neighbor of another requires scanning all possible vertices for every vertex — that is how an adjacency MATRIX works, not an adjacency list.
With adjacency lists, each vertex only looks at its own neighbors — never at every other vertex.
\[ \text{Actual: } T = \Theta(|V| + |E|) \]
Count only real neighbor-checks
Why: A vertex's processing time is proportional to its own degree, not to the total vertex count. Summed over all vertices, that totals twice the edge count.
Confirm with the earlier tally
Why: On the 6-vertex, 7-edge graph, the real operation count was 20 — nowhere near vertices squared (36) — and on a sparse graph with many more vertices than edges, the gap between vertices-plus-edges and vertices-squared grows enormous.
\[ 20 \ll 36 \]
Pattern
1. Confirm the graph is stored as an adjacency list
Why: The vertices-plus-edges bound depends on this representation; an adjacency matrix changes the accounting entirely.
2. Count one unit of work per vertex
Why: Each vertex is dequeued once in BFS, or discovered-and-finished once in DFS — this contributes a term proportional to the number of vertices.
3. Count one unit of work per edge-endpoint examined
Why: Summing every vertex's degree gives at most twice the edge count — this contributes a term proportional to the number of edges.
4. Add the two terms — never multiply them
Why: The total is vertices plus edges, added together, never vertices times edges and never vertices squared. Multiplying or squaring only happens if you mistakenly re-scan the whole vertex set for every vertex.
Check
A graph is stored as an adjacency list and has 500 vertices and 1,200 edges.
Check your understanding
Which best describes the running time of BFS or DFS on this graph?
Answer: A
Why: With an adjacency list, each vertex is processed once and each edge is examined a constant number of times total, so the total work adds the vertex term and the edge term together: proportional to vertices plus edges.
Connect it up
Draw it
Draw the family tree. Put BFS and DFS at the top, and underneath each write the algorithms and problems that are really just that traversal with something added.
Concept
Moves added today: none.
That is a result, not a gap. Everything in this lesson was proved with moves you already owned.
Moves you reused today:
The BFS correctness proof is induction on distance, and the running-time argument is a loop invariant plus dropping constants. No new machinery — new subject matter, old tools.
Full toolkit so far: #1 through #14.
Next session opens with you naming every one of these from memory, before any new material.
Picture it
Animation
Shows: Past some size, constants stop mattering — a rendered Manim animation.
Rendered with Manim.
Takeaway: Traversal sits on the linear rung, not the quadratic one.
Exit ticket
Predict first
Be honest about the weakest spot while it is still fresh.
Correct: Whichever you picked is where tonight's practice should start.
Why: There is no wrong answer here. Naming the weak spot out loud is what converts a vague sense of unease into a specific problem you can actually go and practise, and doing it now while the trace is still in memory costs almost nothing.
Recap
You can now trace and reason about both fundamental graph traversals.
queue, and its parent pointers trace a shortest path measured in edges — but only when every edge counts the same (unweighted graphs).stack, timestamping each vertex with a discovery and finish time.| Traversal | Structure | Guarantee |
|---|---|---|
| BFS | queue | Shortest path by edge count, unweighted graphs only |
| DFS | stack / recursion | Discovery/finish times; reveals cycles via back edges |
Want this taught 1-on-1? Alexander tutors CS3000 Algorithms — $55/session, free consultation.