Uninformed Search (BFS and DFS)

Context: FIT2004_MOC · the W5 graph traversal spine that Dijkstra’s Algorithm and Topological Sort are both built from

Quick Revision

  • 🎯 Objective: ONE loop, ONE parameter ➔ the frontier’s discipline — FIFO (Queue (ADT)) gives BFS, LIFO (Stack (ADT)) gives DFS.
  • 📦 Core Components: frontier / discovered ➔ nodes waiting · visited ➔ stops re-exploration · came_from / previous ➔ records the path · nodes_expanded ➔ the comparison metric.
  • ⚠️ Key Constraint: BFS’s shortest-path guarantee needs FIFO order AND uniform step cost — lose either and it evaporates; on a weighted graph you must move to Dijkstra’s Algorithm.

📝 How It Works

1. The Shared Skeleton

  • One loop, four moves ➔ take a node off the frontier ➔ count it ➔ goal-test it ➔ push its unvisited neighbours on.
  • Only the frontier differs ➔ every other line of BFS and DFS is character-identical; the data structure is the algorithm.
  • Domain-blind ➔ needs only the successor function, so grid, maze and road network run the same code.
  • Termination ➔ goal dequeued ➔ reconstruct path · frontier empties ➔ failure (goal unreachable).

2. Frontier = Queue ➔ BFS

  • FIFO ordering ➔ nodes discovered first are expanded first ➔ all of depth before any of depth ; it radiates outward in layers.
  • Shortest-path guarantee ➔ nodes are reached in nondecreasing distance order, so the first arrival at any node is via a shortest path.
  • The price ➔ it explores nearly every open cell before the goal surfaces, and the fraction explored grows with grid size.

3. Frontier = Stack ➔ DFS

  • LIFO ordering ➔ the newest node is popped ➔ commits to one branch and drives it to the wall before backtracking; a snaking corridor, not a wave.
  • No guarantee ➔ swapping FIFO for LIFO destroys the layer ordering that produced optimality — DFS finds a path, not the shortest.
  • Why keep it ➔ it is the engine of backtracking search, topological sort and cycle detection, and its frontier stays thin.
  • Recursion is the natural form ➔ the call stack is the LIFO frontier, so recursive DFS is the idiomatic implementation (and the one Topological Sort modifies).

4. The Two Bookkeeping Structures

  • visited (a set) ➔ membership test before enqueueing; initialise with start. Without it a cyclic graph re-enqueues forever.
  • came_from (a dict) ➔ came_from[B] = A records B was reached from A; walk it backwards from the goal and reverse ➔ the path.
  • Mark on discovery, not on expansion ➔ add to visited at the moment of enqueue/push; marking at pop lets one node enter the frontier many times.

5. The FIT2004 Graph Formulation (W5)

  • Two states, three words ➔ undiscovered ➔ discovered (in the collection, reachable but unprocessed) ➔ visited (served/popped, edges already scanned). FIT2004 traces carry a Discovered row and a Visited row and both must be written every step.
  • Same skeleton, graph vocabulary ➔ put the source in discovered; while non-empty, serve to visited, and for each edge add if it is neither discovered nor visited.
  • The flag, not a scan ➔ “is already discovered?” must be a attribute (v.discovered), never an search through the queue — the whole bound depends on this.
  • The traversal answer is not unique ➔ it depends on the order edges appear in the adjacency list; state your edge order before tracing.
  • Tiebreakers are always stated ➔ an exam trace fixes the rule (usually alphabetical), and it applies to the insertion order, never to the serve.
  • BFS under an alphabetical rule ➔ append the newly discovered neighbours in alphabetical order; FIFO then serves them in that order.
  • DFS under an alphabetical rule ➔ push them in reverse alphabetical order — LIFO pops the last pushed first, so pushing alphabetically walks the neighbours backwards.
  • Weight is irrelevant here ➔ a weighted graph traverses exactly as its unweighted skeleton unless the tiebreaker names the weight ➔ Dijkstra’s Algorithm is where weight starts to order the serve.
  • What it is a subroutine for ➔ reachability · connected components · cycle detection · shortest path on an unweighted graph · Topological Sort · and, with the collection upgraded to a min-Heap, Dijkstra’s Algorithm.

6. Unweighted Shortest Distance and Path (BFS)

  • One extra line ➔ on discovering from , set v.distance = u.distance + 1 with the source at . Because BFS serves in nondecreasing distance, that first write is already final; v.previous = u at the same moment gives the path.
  • Why DFS cannot do this ➔ LIFO order destroys the layer property, so a DFS “distance” records the depth of the branch you happened to take.
  • Why BFS cannot do the weighted case ➔ per edge is the assumption; with weights the closest vertex is not the one discovered earliest ➔ swap the queue for a priority queue.

7. Many Sources at Once — Change the Input, Not the Algorithm (applied P4)

  • The problem ➔ multi-source shortest path: given sources on an unweighted graph, find for every , in .
  • Solution 1 — modify the algorithm ➔ enqueue all sources at distance before the loop and run BFS unchanged. It still discovers everything at distance from any source before anything at distance , so the first arrival is still final; a vertex already discovered is safely skipped, because whichever source found it first is the closest one.
  • Solution 2 — modify the input ➔ add a super source joined to every , run stock BFS from , then subtract from every distance to pay back the extra hop.
  • They are the same algorithm ➔ ‘s first expansion enqueues exactly at distance ; from there the run is Solution 1 offset by one.
  • Why the sheet prefers Solution 2 (LO1) ➔ modifying a complex algorithm can silently break an invariant and puts its correctness back on you; transforming the input keeps the algorithm a black box, so you owe only one argument — that the transformation preserves the answer ➔ State-Space Graph Modelling.
  • Cost ➔ either way; the super source adds one vertex and edges.

⚙️ Core Implementation

🔹 BFS — unit pseudocode, final form

🔹 DFS — the three-line diff

🔹 DFS without recursion — an explicit stack (applied P9)

🔹 BFS on a graph, with distance and previous (FIT2004 form)

🔹 Path reconstruction

⚖️ Complexity

On a graph stored as an adjacency list (Graph Representations), BFS and DFS have identical bounds.

ResourceBFSDFS
Time (graph, adjacency list)
Space (graph, incl. the list)
Nodes expanded (search tree, worst case)
Frontier sizea whole layer ➔ memory grows with widthone branch ➔ thin
visited + came_from
Optimal (unweighted)✅ shortest❌ any path
  • Where comes from ➔ each vertex is served once () and each undirected edge inspected twice, once from each endpoint () — the discovered flag keeps the per-neighbour test constant.
  • Where the space goes ➔ for the collection plus flags, for the adjacency list itself; quote when the graph is counted, when only auxiliary space is asked for.
  • Adjacency matrix instead ➔ the neighbour scan becomes per vertex ➔ regardless of .

⚖️ Core Decision Matrix

FrontierTrigger conditionProConExploration shape
Queue (FIFO) ➔ BFSshortest path required, edges unweightedprovably optimal; finds a path if one existsexplores broadly; frontier holds a full layerradial wave
Stack (LIFO) ➔ DFSany path suffices; deep/narrow space; backtracking, cycle detection, topological sorttiny frontier; sometimes far fewer expansionsno optimality; can walk long dead-end corridorssnaking corridor
Min-Heap ➔ Dijkstra’s Algorithmedges weighted, all optimal on weighted graphs, needs an index map for updateclosest-first wavefront

When It Flips: on a dead-end grid DFS commits to the corridor, dead-ends and backtracks ➔ a longer path. On a corridor-shaped space with the goal deep along one branch, DFS reaches it after expansions while BFS pays for the entire radius. Uniform cost is the hinge — introduce edge weights and BFS's guarantee dies.

📊 Exam Execution Trace & Applied Exercises

Manual Execution Trace

Undirected graph, adjacency lists in the order shown, source : · · · · · · · .

StepServedNewly discoveredDiscovered afterVisited afterdistance set
,
,
——
( already discovered)
——
——

Final Extracted Output: BFS order · distances , , , . Swapping the queue for a stack gives — same , completely different order, and its depth numbers are not distances.

Applied Exercise — the two orders that are NOT valid

Problem: undirected, adjacency lists · · · · , source . Two candidate BFS orders are offered — and . Give the correct order and kill both.

Final Extracted Output: . dies because only discovers , so must be served first. dies because was served before , so entered the queue before and FIFO must emit it first — both sit at distance , and the queue, not the layer, orders them.

⚠️ Common Mistakes

  • 💡 Omitting the visited check ➔ nodes get re-enqueued after processing, the queue grows without bound and on a cyclic graph the search never terminates.
  • 💡 Returning success instead of a path ➔ at the moment G is dequeued every step that led there is gone; the deliverable is the move sequence, not a boolean.
  • 💡 Exporting BFS optimality to weighted graphs ➔ FIFO orders by hop count; roads with different travel times break the guarantee immediately ➔ Dijkstra’s Algorithm.
  • 💡 Forgetting the super source’s ➔ every distance from is one hop too long; the transformation is only answer-preserving once the offset is paid back.
  • 💡 Quoting without naming the representation ➔ it is an adjacency-list bound; on an adjacency matrix the same code is .
  • 💡 Guessing the order inside a distance layer ➔ same-depth vertices are emitted in queue order, fixed by when each was discovered and by the stated tiebreaker — not by name, and not by the order you drew them.

🧠 Active Recall