Successor swaps empty tile's coords with a neighbour's.
Generic search algorithm
// Inputs: graph, start node s, goal test goal(n)function Search(graph, s, goal):
frontier ← { <s> }
while frontier is not empty:
select and remove a path <n0, …, nk> from frontier
if goal(nk):
return <n0, …, nk>
for each neighbour n of nk:
add <n0, …, nk, n> to frontier
return “no solution”
They differ only in which path they pull from the frontier.
Depth-first search (DFS)
Frontier = stack (LIFO)
Pop newest
Dive deep, then backtrack
Trace: DFS
Push children alphabetically. Step numbers = order of expansion.
Frontier:(S)(D, E, P)(D, E, Q)(D, E)(D, H, R)(D, H, F)(D, H, C, G)goal G reached!
Evaluating search algorithms
Four questions we'll ask about every algorithm:
Space — how much memory? (worst-case frontier size)
Time — how many nodes does it visit? (worst case)
Complete? — is it guaranteed to find a goal if one exists?
Optimal? — does it return the cheapest (or shallowest) goal?
Tree parameters: \(b\), \(m\), \(d\)
\(b\) — branching factor
\(d\) — depth of shallowest goal
\(m\) — max depth of tree
DFS — properties
Space
\(O(bm)\) linear
Time
\(O(b^m)\) exponential
Complete?
No
Optimal?
No
DFS fails on cycles
\(H \to I \to J \to H\) traps DFS forever.
Fix: cycle pruning
Keep a visited set. Skip states we've already expanded.
Order: S → H → I → J → back to H? skip! → G.
DFS takes the first goal, not the best
Returns the long path; ignores the shorter \(S \to D \dashrightarrow G\).
When to use DFS
Good fit
Memory limited
Many goals exist
Deep solutions
Bad fit
Cycles
Shallow solutions
Need optimality
Breadth-first search (BFS)
Frontier = queue (FIFO)
Pop oldest
Level by level
Trace: BFS
Pop the oldest path. Step number = order of expansion. Multiple paths to P and B.
Frontier:(S)pop S → push D, E, P → (D, E, P)pop D → push B, C → (E, P, B, C)pop E → push H, J → (P, B, C, H, J)pop P → push Q → (B, C, H, J, Q)pop B → push P → (C, H, J, Q, P)pop C, H, J (leaves) → (Q, P)pop Q → push B, G → (P, B, G)pop P, B → push Q, P → (G, Q, P)pop G — goal!
BFS — properties
Space
\(O(b^d)\) exponential
Time
\(O(b^d)\) exponential
Complete?
Yes
Optimal?
Shallowest goal (optimal at equal cost)
When to use BFS
Good fit
Memory plentiful
Want fewest arcs
Cycles possible
Bad fit
Deep solutions
Large branching
Graph generated on the fly
What if edges have different costs?
S→A→G: cost 11. S→B→G: cost 6. BFS picks the wrong one (it's fewer hops, but pricier).
Uniform-cost search (UCS)
Frontier = priority queue
Key = path cost so far
Pop the cheapest path first
Complete + optimal when all costs are positive.
When every edge has cost 1, UCS = BFS. (We'll use BFS in this lecture for that reason.)
BFS vs DFS — Q1
Q. Memory is very limited.
BFS
DFS
Both are fine
Neither
B — DFS. DFS: \(O(bm)\) vs BFS: \(O(b^d)\).
BFS vs DFS — Q2
Q. All solutions are deep in the search tree.
BFS
DFS
Both are fine
Neither
B — DFS. Dives down long paths instead of enumerating every shallower level.
BFS vs DFS — Q3
Q. The graph contains cycles.
BFS
DFS
Both are fine
Neither
A — BFS. DFS can fail to find the goal (gets stuck going deeper into the cycle); BFS still finds any goal at finite depth. (BFS does waste work without cycle pruning — but it's complete.)
BFS vs DFS — Q4
Q. The branching factor is huge or infinite.
BFS
DFS
Both are fine
Neither
B — DFS. BFS's frontier blows up with \(b\); DFS stays linear.
BFS vs DFS — Q5
Q. You must find the shallowest goal.
BFS
DFS
Both are fine
Neither
A — BFS. BFS guarantees the shallowest goal.
BFS vs DFS — Q6
Q. All solutions are very shallow.
BFS
DFS
Both are fine
Neither
A — BFS. Checks shallow neighbours before diving deep.