Search problems, BFS, DFS & UCS
Write any planning puzzle down as a formal search problem, then hand-trace breadth-first, depth-first and uniform-cost search on it — including the frontier bookkeeping step that everyone gets wrong.
How to write a planning puzzle down as a formal search problem, and how BFS, DFS and UCS differ from each other by a single line.
- Search problem — a state space, $\text{Actions}(s)$, $\text{Result}(s,a)$, a step cost $c(s,a,s')$, a start state $s_0$, a goal test $\text{IsGoal}(s)$. A solution is a plan (an action sequence), never a state.
- Node —
state, parent, action, path-cost, with $g(\text{child}) = g(\text{parent}) + c(\text{parent.state}, a, \text{child.state})$. The plan is recovered by followingparentback from the goal. - Graph vs tree — a state space graph holds each state once; a search tree holds one node per path, and is infinite whenever the graph has a cycle. Graph search = tree search + an explored set.
- The frontier discipline is the only difference — FIFO ⇒ BFS, LIFO ⇒ DFS, priority $g(n)$ ⇒ UCS, priority $g(n)+h(n)$ ⇒ A*.
- BFS — complete if $b$ is finite; optimal only when all step costs are equal; $O(b^d)$ time and $O(b^d)$ space.
- DFS — as tree search neither complete nor optimal; $O(b^m)$ time but only $O(bm)$ space.
- UCS — complete if every step cost $\ge \varepsilon > 0$; optimal; $O\!\left(b^{1+\lfloor C^*/\varepsilon\rfloor}\right)$ time and space. It expands increasing cost contours, in every direction at once.
- UCS frontier rules — one node per state; a cheaper rediscovery replaces it (parent pointer included); a no-better one is discarded; an already-explored state is ignored. The goal test is on pop, not on generation.
- Abstraction — keep a variable in the state iff the goal test, the action set or the cost function reads it. That is what turns $120 \cdot 2^{30} \cdot 12^2 \cdot 4$ into $120$.
Everything below is the explanation and the practice. If you are short on time, read this box, then go to the problems.
1. What a search problem actually is
An agent can always answer three local questions: where am I, what moves are legal here, and if I take one, where do I land and what did it cost? A search problem is those three, written down formally, plus a goal test.
A search problem consists of:
- a state space — the set of all configurations the world can be in, as far as our problem cares;
- for each state $s$, a set $\text{Actions}(s)$ of allowable actions;
- a transition model (successor function) $\text{Result}(s,a)$ — the state you land in if you do $a$ in $s$;
- a step cost function $c(s,a,s')$ — the cost of the single action $a$ that took you from $s$ to $s' = \text{Result}(s,a)$;
- a start state $s_0$ and a goal test — a predicate $\text{IsGoal}(s)$ that returns true or false.
A solution is a sequence of actions — a plan — which transforms the start state into a goal state. An optimal solution is one of minimum total cost.
One symbol to read carefully: $\text{Result}(s,a)$ is a function — one state and one action in, one state out, deterministic. (A random ghost breaks this and needs expectimax.)
Notice what is not in the definition: no map, no global view. A search algorithm may only call $\text{Actions}$, $\text{Result}$, $c$ and $\text{IsGoal}$; everything it knows, it discovered by poking those four functions.
A solution is a plan, not a state
Asked “what is the solution?”, the answer is an action sequence — [North, North, East, East] — or the path of states it induces, S → A → C → G. Never just the goal state.
“The answer is $G$.” No — $G$ was given to you in the goal test. Search recovers the plan, and we recover it by following parent pointers back from the goal node, not by remembering paths as we go.
2. World states, search states, and how big the space is
This slide looks like arithmetic and is really about modelling. Pacman in a small maze:
World state:
- Agent positions: 120
- Food count: 30
- Ghost positions: 12
- Agent facing: N, S, E, W
| Task | State representation | State space size |
|---|---|---|
| World states | $(p, f_1, \dots, f_{30}, g_1, g_2, d)$ | $120 \times 2^{30} \times 12^2 \times 4$ |
| Eat the top-left dot | $(p)$ | $120$ |
| Eat all dots | $(p, f_1, \dots, f_{30})$ | $120 \times 2^{30}$ |
$p$ is Pacman's position, one of 120 non-wall squares. $f_i$ is a boolean — is dot $i$ still there? — so 30 dots give $2^{30}$ combinations: the 30 is a count of bits, not a count of states. $g_1, g_2$ are the ghosts' positions, $12 \times 12 = 12^2$; $d$ is the facing, 4 values. Multiply: $120 \cdot 2^{30} \cdot 144 \cdot 4 \approx 7.4 \times 10^{13}$, seventy-four trillion.
Why the representation shrinks when the task shrinks
A variable belongs in your state representation if and only if (a) it changes as you act, and (b) the legal actions, the costs or the goal test depend on it. Anything else is decoration and you must drop it, because every bit you keep doubles the size of the space.
For “eat the top-left dot” the goal test reads only Pacman's square. Ghosts do not block movement and dying is not modelled, so they go — factor $144$. Which other dots are eaten changes nothing we can do next, so the food bits go — factor $2^{30}$. Legal moves ignore the facing — factor $4$. State $= (p)$, 120 states.
For “eat all dots” the goal test is “are all 30 $f_i$ false?”, which mentions the food, so the food stays: $120 \cdot 2^{30} \approx 1.3 \times 10^{11}$. Ghosts and facing are still never read.
World state vs. search state
A world state contains every detail of the environment; a search state contains only what the search problem needs. Choosing it is abstraction — the slide's “vertices are (abstracted) world configurations.” An abstraction must be sound: every plan that works in the abstract problem must correspond to one that works in the real one.
Abstraction is the highest-leverage decision in applied search: the algorithms are fixed and ten lines long, so the gap between a millisecond and never finishing is almost always the representation.
Same maze as the slide: 120 legal squares, 30 dots, 2 ghosts each in one of 12 squares, 4 facings. Give the state representation and the state-space size for each task.
- (a) “Reach the bottom-right corner without ever being caught by a ghost”, where ghosts move and catching means sharing a square.
- (b) “Eat all 30 dots” in a version where Pacman can only turn left or go straight (no reversing).
- (c) “Eat the 4 power pellets (a subset of the 30 dots), in any order.”
Step 1 — Run the relevance test on each variable
For each of $p$, the $f_i$, $g_1,g_2$ and $d$, ask: does this task's goal test, action set, or cost function read that variable? If the variable is read anywhere, or if it changes in a way that later affects one of those, it stays.
Step 2 — (a) the ghost-avoiding walk
The goal test reads only $p$. But the legality of the plan now depends on where the ghosts are — “caught” is defined by $p = g_i$ — so ghost positions are read by the problem. Food is never read; facing is never read.
State $= (p, g_1, g_2)$, size $120 \cdot 12 \cdot 12 = 120 \cdot 144 = \mathbf{17{,}280}$.
Still tiny. This is the classic lesson: adding adversaries costs you a polynomial factor, adding memory of what you've collected costs you an exponential one.
Step 3 — (b) all dots, but no reversing
The goal test reads all the $f_i$, so keep the food bits: $2^{30}$. New wrinkle: $\text{Actions}(s)$ now depends on which way you are facing, because “go straight” and “turn left” are defined relative to $d$. Since the action set reads $d$, $d$ is now part of the state.
State $= (p, f_1,\dots,f_{30}, d)$, size $120 \cdot 2^{30} \cdot 4 \approx \mathbf{5.2 \times 10^{11}}$.
Moral: a rule change that doesn't touch the goal can still force a variable back into the state, through the action set.
Answer & the takeaway
(c) Only 4 of the 30 dots are ever read by the goal test, so you only need 4 booleans, not 30:
State $= (p, f_{i_1}, f_{i_2}, f_{i_3}, f_{i_4})$, size $120 \cdot 2^4 = 120 \cdot 16 = \mathbf{1{,}920}$.
Takeaway: $10^{11}$ became $10^3$ purely by noticing that 26 of the booleans are never read. Before you optimise an algorithm, delete a variable.
3. State space graphs vs. search trees
Two pictures, constantly confused, drawn from the same problem.
The state space graph is a mathematical representation of a search problem: vertices are the (abstracted) world configurations, edges are transitions resulting from actions, edge weights are step costs, and the goal test picks out a set of goal vertices (maybe only one). In a state space graph, each state occurs exactly once.
We can rarely build this full graph in memory — the Pacman graph has $10^{13}$ vertices — but it is the right mental object.
The search tree is what an algorithm actually builds. Its root is the start state. Each node corresponds to a path from the start, and its children are the results of the legal actions at the end of that path. A state appears once in the graph but once per path in the tree.
The search tree is the graph unrolled along every path: if two routes reach $B$, the graph has one $B$ and the tree has two nodes labelled $B$ — same state, different histories, usually different path costs.
S-B, S-A-B, S-B-A-B). Because A and B point at each other, the tree never ends — a finite graph can unroll into an infinite tree. The only thing stopping it is an algorithm that refuses to re-expand a state it has already seen.Tree nodes are paths; graph vertices are states. The tree is infinite whenever the graph has a cycle, even with three vertices. “Graph search” is just tree search plus a memory of which states we already expanded.
q four separate times.4. The generic search algorithm
Every algorithm on this page — and greedy search and A* on the next page — is the same seven lines with one word changed.
The frontier
The frontier (fringe, open list) holds the nodes that have been generated but not yet expanded — the rim of what we know. The explored set (closed list) holds the states already expanded. To expand a node: take its state, call $\text{Actions}$ and $\text{Result}$, and put the children on the frontier.
Tree search
function TREE-SEARCH(problem) returns a solution, or failure
initialize the frontier using the initial state of problem
loop do
if the frontier is empty then return failure
choose a leaf node and remove it from the frontier <-- THE ONLY CHOICE
if the node contains a goal state then
return the corresponding solution
expand the chosen node, adding the resulting nodes to the frontier
The comment marks the entire design space of uninformed search. Choose the shallowest node and you have BFS. Choose the deepest and you have DFS. Choose the cheapest-so-far and you have UCS. Choose by $g+h$ and you have A*. That is the whole story.
Graph search
function GRAPH-SEARCH(problem) returns a solution, or failure
initialize the frontier using the initial state of problem
initialize the explored set to be empty
loop do
if the frontier is empty then return failure
choose a leaf node and remove it from the frontier
if the node contains a goal state then
return the corresponding solution
add the node's state to the explored set
expand the chosen node, adding the resulting nodes to the frontier
only if not already in the frontier or the explored set
Two lines added, one enormous consequence: each state is expanded at most once, so the search is finite whenever the state space is. The cost is memory — the explored set can grow to the size of the state space.
“Graph search is strictly better, always use it.” Not quite. It needs $O(|S|)$ memory for the explored set, which kills it on huge spaces (that $10^{13}$-vertex Pacman graph). And the “only if not already in the frontier” clause is the subtle one: for UCS and A* you may not simply discard a duplicate — you must compare costs and possibly replace (§10). Getting that wrong is the most common error on this material.
What a node actually stores
From the slide “A Note on Implementation”. A node is a record with four fields:
| Field | Meaning | Value for a child of parent via action $a$ |
|---|---|---|
state | the state this node sits at | Result(parent.state, a) |
parent | the node we came from | parent |
action | the action taken to get here | a |
path-cost | total cost from the start to here; written $g(n)$ | parent.path_cost + c(parent.state, a, state) |
The last row is the one to memorise, because it is the definition of $g$:
$$g(\text{child}) \;=\; g(\text{parent}) \;+\; c(\text{parent.state},\, a,\, \text{child.state}).$$In words: a path costs its prefix plus the one new step. $g(n)$ is the backward cost — what you have already spent — as against the forward estimate $h(n)$ on the next page. We store parent rather than whole paths to save memory.
Start at the goal node. Repeatedly follow parent, collecting the action field at each hop, until you reach the node whose parent is null (the start). Reverse the collected list. That is your plan.
5. Breadth-first search
Strategy: expand a shallowest node first. Implementation: the frontier is a FIFO queue.
FIFO = first in, first out: children join the back, we pop from the front, so the queue drains depth 0, then all of depth 1, then all of depth 2 — BFS sweeps the tree in complete tiers. It is a ripple on a pond, except that the ripple travels by number of actions, not by cost, which is exactly why UCS exists.
Properties, with the reasoning
Write $b$ for the branching factor (max children per node), $d$ for the depth of the shallowest goal, $m$ for the maximum depth of the tree (possibly $\infty$).
- Complete? Yes, if $b$ is finite — a goal sits at some finite depth $d$, every tier is finite, so BFS reaches it.
- Optimal? Only when all step costs are equal. BFS returns the goal with the fewest actions; if every action costs the same, fewest actions = cheapest. With unequal costs it returns a 2-step path of cost $100$ over a 3-step path of cost $3$.
- Time: $O(b^d)$ — it generates every node down to depth $d$, and $1 + b + \dots + b^d = \dfrac{b^{d+1}-1}{b-1} = O(b^d)$: the last tier dominates the sum.
- Space: $O(b^d)$, and this is the killer — just before expanding the first node of tier $d$, the frontier holds essentially all of tier $d$: $b^d$ nodes in memory at once.
$b=10$, one million nodes per second, 100 bytes per node, $d=12$: time $\approx 10^{12}$ nodes $= 13$ days; memory $\approx 10^{14}$ bytes $= 100$ terabytes. BFS runs out of RAM long before it runs out of patience. Remember this when you meet iterative deepening in alpha-beta: memory, not time, is usually what stops you.
6. Depth-first search
Strategy: expand a deepest node first. Implementation: the frontier is a LIFO stack.
Change one word — FIFO becomes LIFO — and the behaviour inverts: children go on top of the stack and one is popped immediately, so the search plunges down a branch until it dead-ends, then backs up one level. Like a maze-walker with chalk, you mark only the current path — DFS's $O(bm)$ memory advantage.
Properties, with the reasoning
- Complete? Tree search: no — one cycle and DFS walks $A, B, A, B, \dots$ forever, and an infinitely deep branch traps it too. Graph search on a finite space: yes, because the explored set forbids revisiting.
- Optimal? No, in no sense. It returns the first goal it stumbles into, however deep and expensive.
- Time: $O(b^m)$ — in the worst case it explores the whole tree down to maximum depth $m$. When $m \gg d$ that is far worse than BFS; when goals are dense it can be far better in practice.
- Space: $O(bm)$ — linear: the stack holds only the current root-to-leaf path ($m$ nodes) plus their unexpanded siblings ($b-1$ each). This is the entire reason anyone still uses DFS.
BFS buys optimality-in-steps with exponential memory; DFS buys linear memory by giving up optimality and (in cyclic spaces) completeness. Iterative deepening buys back BFS's guarantees at DFS's memory; UCS buys back true cost-optimality.
7. Iterative deepening beyond syllabus
Not in the Lecture 2 slides, but standard pre-reading (R&N §3.4.5), and alpha-beta leans on it for move ordering.
Run a depth-limited DFS with limit $0$; no goal, run it again with limit $1$, then $2$, stopping at the first limit that finds a goal.
Re-searching from scratch every round sounds wasteful. It is not, and the arithmetic is worth seeing. Nodes generated by IDS down to depth $d$:
$$N_{\text{IDS}} = (d+1)b^0 + d\,b^1 + (d-1)b^2 + \dots + 1\cdot b^d,$$because the root is regenerated $d+1$ times, tier 1 $d$ times, and the bottom tier exactly once. BFS generates $N_{\text{BFS}} = b^0 + b^1 + \dots + b^d$. For $b = 10, d = 5$: $N_{\text{IDS}} = 123{,}456$ versus $N_{\text{BFS}} = 111{,}111$ — about 11% more work, and in general the overhead factor tends to $\dfrac{b}{b-1}$.
The bottom tier dominates, and IDS generates it only once. So IDS is complete, optimal when step costs are uniform, $O(b^d)$ time and only $O(bd)$ memory — the standard choice for large uniform-cost spaces of unknown depth.
8. Uniform-cost search
BFS optimises the wrong thing: number of actions. Real problems have costs — time, fuel, money, risk. UCS fixes exactly this and nothing else.
Strategy: expand a cheapest node first. Implementation: the frontier is a priority queue, with priority = cumulative path cost $g(n)$.
That's it. Same skeleton as BFS, different queue discipline. When all step costs are equal, $g(n)$ is proportional to depth and UCS degenerates into BFS.
Here is the course's pseudocode, from the slides:
function UNIFORM-COST-SEARCH(problem) returns a solution, or failure
initialize the explored set to be empty
initialize the frontier as a priority queue using g(n) as the priority
add initial state of problem to frontier with priority g(S) = 0
loop do
if the frontier is empty then
return failure
choose a node and remove it from the frontier <-- the MINIMUM-g node
if the node contains a goal state then
return the corresponding solution
add the node state to the explored set
for each resulting child from node
if the child state is not already in the frontier or explored set then
add child to the frontier
else if the child is already in the frontier with higher g(n) then
replace that frontier node with child
Three lines deserve a sentence each.
- “choose a node and remove it from the frontier” — pop the node with the smallest $g$. Ties break arbitrarily, giving different but equally optimal answers.
- “if the node contains a goal state then return” — the goal test is on pop, and this is required for optimality, not stylistic: a goal generated cheaply may still sit on the frontier with a bad $g$ that a later path improves. Only at the front of the queue do we know nothing cheaper remains.
- “else if the child is already in the frontier with higher $g(n)$ then replace” — the decrease-key step of §10: rediscover a frontier state more cheaply and you must upgrade its entry; if the new route is worse, discard it. If the state is already explored, do nothing — safe by §13, because a popped node's $g$ is already optimal.
Properties, with the reasoning
Let $C^*$ be the cost of the optimal solution and $\varepsilon > 0$ a lower bound on any single step cost.
- Complete? Yes, provided every step cost is at least some $\varepsilon > 0$ and a finite-cost solution exists: each expansion raises the contour by at least $\varepsilon$, so after finitely many expansions the frontier minimum would exceed $C^*$ — impossible unless the goal was popped first. The $\varepsilon$ is not pedantry: with zero-cost actions UCS can loop forever at $g = 0$.
- Optimal? Yes — see the proof in §13.
- Time and space: $O\!\left(b^{\,1 + \lfloor C^*/\varepsilon \rfloor}\right)$, because UCS expands every node whose path cost is less than $C^*$, and at $\varepsilon$ per step the deepest such node sits at depth about $C^*/\varepsilon$ — an “effective depth” replacing BFS's $d$. With unit costs $C^*/\varepsilon = d$ and the bound collapses back to $O(b^{d+1})$.
Writing UCS's complexity as $O(b^d)$. The goal's depth is irrelevant to UCS; what matters is how many cheap nodes exist. A goal 2 steps away costing $1000$ is found only after every node reachable for less than $1000$ has been expanded — possibly millions, at depth 500.
9. Watch the frontier move
Press Step and follow three columns: what got popped, what is on the frontier, and what is already explored. Then switch algorithm and run the same graph again.
Using the graph in Figure 2 (edges $S\!-\!A\,{=}\,1$, $S\!-\!B\,{=}\,6$, $A\!-\!C\,{=}\,2$, $A\!-\!E\,{=}\,4$, $C\!-\!D\,{=}\,2$, $D\!-\!G\,{=}\,2$, $E\!-\!G\,{=}\,6$, $B\!-\!G\,{=}\,3$; neighbours are tried in the order listed in the adjacency list $S\!:\![A,B]$, $A\!:\![S,E,C]$, $B\!:\![S,G]$, $C\!:\![A,D]$, $D\!:\![C,G]$, $E\!:\![A,G]$), predict:
- Which path does each of BFS, DFS and UCS return, and at what cost?
- How many nodes does each one pop before returning?
Step 1 — What is each algorithm actually minimising?
BFS minimises the number of edges. Look for the shortest path by hop count: $S\!-\!B\!-\!G$ is 2 hops; everything else is 3 or 4. So BFS must return $S\!-\!B\!-\!G$, whatever it costs.
DFS minimises nothing. It returns whatever it bumps into while diving. Follow the adjacency order: from $S$ it takes $A$ first; from $A$ it skips $S$ (explored) and takes $E$ first; from $E$ it takes $G$. Done — $S\!-\!A\!-\!E\!-\!G$.
UCS minimises total cost. Enumerate the four paths: $S\!-\!A\!-\!C\!-\!D\!-\!G = 1{+}2{+}2{+}2 = 7$; $S\!-\!B\!-\!G = 6{+}3 = 9$; $S\!-\!A\!-\!E\!-\!G = 1{+}4{+}6 = 11$; $S\!-\!B\!-\!D\!-\!G$ doesn't exist. So UCS returns the 7.
Step 2 — Count BFS's pops
Frontier as a FIFO. Pop $S$ → push $A(1), B(6)$. Pop $A$ → push $E, C$. Pop $B$ → push $G$. Pop $E$ → $G$ already on the frontier, skip. Pop $C$ → push $D$. Pop $G$ → goal.
6 pops: S, A, B, E, C, G. Cost of the answer: $9$. BFS found a path with fewer edges but 29% more cost than optimal.
Step 3 — Count DFS's and UCS's pops
DFS: pop $S$ (push $B$ then $A$ so that $A$ is on top), pop $A$ (push $C$ then $E$), pop $E$ (push $G$), pop $G$. 4 pops — by far the fastest — for the worst answer, cost $11$.
UCS: pop $S(0)$; pop $A(1)$ pushing $E(5), C(3)$; pop $C(3)$ pushing $D(5)$; tie between $E(5)$ and $D(5)$ — the frontier had $E$ first, so pop $E(5)$, pushing $G(11)$; pop $D(5)$, which generates $G$ at $5{+}2 = 7 < 11$, so it replaces the frontier entry; pop $B(6)$, which generates $G$ at $6{+}3 = 9 > 7$, so it is discarded; pop $G(7)$ → goal. 7 pops.
Answer & the takeaway
| Path returned | Cost | Edges | Pops | |
|---|---|---|---|---|
| BFS | S-B-G | 9 | 2 | 6 |
| DFS | S-A-E-G | 11 | 3 | 4 |
| UCS | S-A-C-D-G | 7 | 4 | 7 |
Takeaway: each algorithm wins on the thing it optimises and loses on the others. BFS wins on hops, DFS wins on effort, UCS wins on cost — and cost is usually what the problem actually asked for. Also note that the optimal path is the longest one in edges: this is precisely the situation where BFS silently gives you a wrong answer that looks plausible.
10. The UCS walkthrough, and the step everyone gets wrong
This is the slide trace, reproduced exactly and then explained. The graph:
The slide's table, complete:
| Frontier (added, with $g$) | Explored | What happened |
|---|---|---|
S: 0 | S | start; pop $S$, expand it |
S-A: 1 | A | pushed by $S$; cheapest, so popped next |
S-B: 4 | C | pushed by $S$; waits |
S-A-C: 3 | B | pushed by $A$; $3 < 4$ so popped before $B$ |
S-A-C-D: 7 | D | pushed by $C$ |
S-B-D: 5 | replaced the 7 — better path to D! | |
S-B-D-G: 8 | pushed by $D$; popped; goal |
Result: S-B-D-G, path cost 8.
The step you must understand: “replaced by a better path to D!”
Freeze the algorithm right after $C$ has been expanded. The state of the world is:
- Explored: $\{S, A, C\}$
- Frontier: $\{\;\texttt{S-B}: 4,\;\; \texttt{S-A-C-D}: 7\;\}$
UCS pops the minimum, which is S-B at $4$. It expands $B$. $B$'s only child is $D$, reached with
Now run the pseudocode's last two lines against this child:
- “if the child state is not already in the frontier or explored set” — $D$ is already in the frontier, as
S-A-C-Dwith $g = 7$, so we do not simply add it. - “else if the child is already in the frontier with higher $g(n)$, replace that frontier node with child” — the entry has $g = 7$, our new child has $g = 5$, and $5 < 7$, so we throw the old node away entirely and put the new one in its place.
The frontier now holds exactly one node for state $D$: S-B-D with $g = 5$, and S-A-C-D no longer exists anywhere. Hence the slide's annotation “Replaced by better path to D!”.
A state gets at most one frontier node, and it carries the cheapest path found so far. Rediscovery either upgrades that node (new $g$ smaller) or is discarded (new $g$ no smaller). The upgrade also rewrites the parent pointer — which is how the final answer comes out as $S\!-\!B\!-\!D\!-\!G$ and not $S\!-\!A\!-\!C\!-\!D\!-\!G$.
- Keeping both. Writing a frontier with
S-A-C-D: 7andS-B-D: 5on it. You then expand $D$ twice and the trace never matches the answer key. One state, one frontier node. - Discarding the new node because “$D$ is already there.” That is the rule for BFS and DFS, where all that matters is reaching a state. For UCS, how expensively you reached it matters, so you must compare.
- Forgetting to update the parent pointer. Students update the number 7 → 5 but leave the path reading
S-A-C-D, then report the final answer as $S\!-\!A\!-\!C\!-\!D\!-\!G$ with cost 8 — a path that costs 10 and an answer that is internally inconsistent.
Why is the replacement safe? $D$'s future cost does not depend on how we reached $D$, so the only difference between the two nodes is the prefix, and $5 < 7$: any completion of the expensive prefix is beaten, edge for edge, by the same completion of the cheap one. That optimal substructure fails as soon as costs depend on history (a toll charged only if you passed through $C$) — your cue that the history belongs in the state.
Note also that $S\!-\!A$ is the cheapest first move, and UCS briefly preferred it — it popped $A$ before $B$ — before the arithmetic overturned it. Greedy first steps are not optimal plans.
Trace UCS (graph search, goal test on pop) on this graph. Write down, for every iteration: the node popped, the explored set after it, and the full frontier. Then give the returned path and its cost.
Edges: $S\!\to\!A=2$, $S\!\to\!B=3$, $A\!\to\!C=7$, $A\!\to\!D=9$, $B\!\to\!D=1$, $C\!\to\!G=1$, $D\!\to\!G=6$.
Step 1 — Set up and do the first two pops
Frontier: S:0. Explored: $\{\}$.
Pop 1: $S$ ($g=0$). Not a goal. Explored $=\{S\}$. Children: $A$ at $0{+}2=2$ (new → push), $B$ at $0{+}3=3$ (new → push).
Frontier: S-A:2, S-B:3.
Pop 2: minimum is S-A:2. Not a goal. Explored $=\{S,A\}$. Children: $C$ at $2{+}7=9$ (new → push), $D$ at $2{+}9=11$ (new → push).
Frontier: S-B:3, S-A-C:9, S-A-D:11.
Step 2 — The replacement
Pop 3: minimum is S-B:3. Not a goal. Explored $=\{S,A,B\}$. Its only child is $D$, at $3{+}1 = 4$.
$D$ is already on the frontier as S-A-D with $g=11$. Compare: $4 < 11$, so replace. The node S-A-D:11 is deleted; S-B-D:4 takes its place, with its parent pointer now aimed at $B$.
Frontier: S-B-D:4, S-A-C:9. Note the frontier got shorter, not longer — replacement is not insertion.
Step 3 — The rejection, and why we don't stop at the first goal
Pop 4: minimum is S-B-D:4. Not a goal. Explored $=\{S,A,B,D\}$. Child $G$ at $4{+}6 = 10$; $G$ is new → push.
Frontier: S-A-C:9, S-B-D-G:10.
A goal node is now on the frontier at cost 10. We do not return it. $9 < 10$, so something cheaper is still pending.
Pop 5: minimum is S-A-C:9. Not a goal. Explored $=\{S,A,B,D,C\}$. Child $G$ at $9{+}1 = 10$. $G$ is on the frontier with $g=10$; the new $g$ is $10$, which is not lower, so we discard the new node and keep the existing one. (Had the $C\!\to\!G$ edge cost $0.5$, we would have replaced instead.)
Frontier: S-B-D-G:10.
Answer & the takeaway
Pop 6: S-B-D-G at $g=10$. Goal test passes → return.
Answer: the plan $S \to B \to D \to G$, cost 10. Verify by brute force: $S\!-\!A\!-\!C\!-\!G = 2{+}7{+}1 = 10$; $S\!-\!A\!-\!D\!-\!G = 2{+}9{+}6 = 17$; $S\!-\!B\!-\!D\!-\!G = 3{+}1{+}6 = 10$. Two optima at 10; UCS returned the one that reached the front of the queue first. Optimal does not mean unique.
Takeaway: in six pops you exercised all three frontier rules — push a new state, replace a worse one, discard a no-better one — plus the goal-test-on-pop rule. If you can do this trace cold, you can do A* on the next page, because A* is literally this with the priority changed from $g$ to $g+h$.
11. Cost contours: how UCS really spreads
The slides summarise UCS as “UCS explores increasing cost contours”, beside a picture of concentric rings. Here is what it means.
The contour at cost $c$ is the set of states whose cheapest path from the start costs at most $c$. UCS pops the minimum $g$ and popped $g$-values never decrease (§13), so it expands states in non-decreasing order of optimal cost: the explored set always is a contour.
This also explains the bound $O(b^{1+\lfloor C^*/\varepsilon\rfloor})$ from §8: the expansions are the states inside the $C^*$ contour, and with a minimum step cost $\varepsilon$ you can only be $C^*/\varepsilon$ steps deep inside it.
The contours on a real search tree
The lecture's lettered graph:
Unrolled into a tree the way the slide does it, those twelve states explode to 28 nodes — $q$ appears four times, $a$ three times, $c$, $f$ and $p$ twice each. Drag the slider to sweep the cost contour:
G at $14$ and c at 11 and 15 are states the tree cannot tell apart.Using Figure 5:
- Which path does UCS (graph search) return, and what does it cost?
- Which states does it expand, in order?
- Which states are in the contour $c \le 5$?
- How much work did the explored set save compared with tree search?
Step 1 — Pops 1 to 4
Frontier S:0. Pop $S$; push $d\!:\!3$, $e\!:\!9$, $p\!:\!1$.
Pop $p$ (1). Child $q$ at $1{+}15 = 16$ → push. Frontier: $d\,3$, $e\,9$, $q\,16$.
Pop $d$ (3). Children: $b$ at $4$ (new), $c$ at $11$ (new), and $e$ at $3{+}2 = 5$. But $e$ is already on the frontier at $9$, and $5 < 9$ → replace: $e$ becomes $5$ with parent $d$. Frontier: $b\,4$, $e\,5$, $c\,11$, $q\,16$.
Pop $b$ (4). Child $a$ at $6$ → push.
Step 2 — Pops 5 to 9, and the answer
Pop $e$ (5). Children $h$ at $5{+}8 = 13$, $r$ at $5{+}2 = 7$ → push both.
Pop $a$ (6). No outgoing edges; nothing happens.
Pop $r$ (7). Child $f$ at $8$ → push.
Pop $f$ (8). Children: $c$ at $8{+}3 = 11$ — $c$ is on the frontier at $11$ already and $11 \not< 11$, so discard; $G$ at $8{+}2 = 10$ → push.
Frontier now: $G\,10$, $c\,11$, $h\,13$, $q\,16$. Pop the minimum, $G$ at $10$ → goal test passes.
Answer: $S \to d \to e \to r \to f \to G$, cost $3{+}2{+}2{+}1{+}2 = 10$.
Step 3 — The contour at $c \le 5$
The states whose optimal cost from $S$ is at most 5 are $S(0)$, $p(1)$, $d(3)$, $b(4)$, $e(5)$. Five of the twelve states. Those are exactly the first five pops, in exactly that order — the pop order is the sorted contour.
Set the Figure 6 slider to 5 and you will see these five states, but spread over seven tree nodes, because $e$ and $p$ each appear more than once at low cost.
Answer & the takeaway
Work comparison. Graph-search UCS expanded 8 nodes ($S, p, d, b, e, a, r, f$) and then popped the goal — and it never touched $c$, $h$ or $q$ at all, because their costs exceed $C^* = 10$.
Tree search on the same graph generates all 28 nodes shown in Figure 6 if run to exhaustion — more than triple the work, for the same answer, because it re-expands $q$ four times, $a$ three times, and so on. On a graph with a cycle the ratio would be infinite.
Takeaway: the explored set is not a micro-optimisation; it is the difference between polynomial and exponential blow-up on graphs with many paths between the same pair of states. This is the real content of the pre-reading's “tree search vs graph search.”
12. The comparison table, and how to reconstruct it
| Algorithm | Frontier | Complete? | Optimal? | Time | Space |
|---|---|---|---|---|---|
| BFS | FIFO queue | Yes ($b$ finite) | Only if step costs are all equal | $O(b^{d})$ | $O(b^{d})$ |
| DFS (tree) | LIFO stack | No — cycles or infinite depth trap it | No | $O(b^{m})$ | $O(bm)$ |
| DFS (graph, finite space) | LIFO stack + explored set | Yes | No | $O(|S|)$ | $O(|S|)$ |
| IDS | LIFO stack, restarted | Yes ($b$ finite) | Only if step costs are all equal | $O(b^{d})$ | $O(bd)$ |
| UCS | Priority queue on $g(n)$ | Yes (step costs $\ge \varepsilon > 0$) | Yes | $O\!\left(b^{1+\lfloor C^{*}/\varepsilon\rfloor}\right)$ | $O\!\left(b^{1+\lfloor C^{*}/\varepsilon\rfloor}\right)$ |
Derive it from two questions. (1) What does the frontier hold at its worst moment? A whole tier ⇒ $b^d$ space (BFS, UCS); one path plus siblings ⇒ $bm$ space (DFS, IDS). (2) What must the algorithm look at before it can stop? Everything shallower than the goal ⇒ $b^d$ time; everything cheaper ⇒ $b^{C^*/\varepsilon}$ time. Optimality falls out of (2): if you examine everything better than what you return, you cannot have missed anything better.
13. Why UCS is complete and optimal — the proof
The slides assert “UCS is complete and optimal!” and move on. Here is the argument. Assume every step cost satisfies $c(s,a,s') \ge \varepsilon > 0$.
Claim. If UCS pops node $n_1$ and then later pops $n_2$, then $g(n_1) \le g(n_2)$.
Proof. When $n_1$ is popped it is the minimum of the frontier, so every node then on the frontier has $g \ge g(n_1)$. The only nodes ever added afterwards are children of expanded nodes, and a child's cost is $g(\text{parent}) + c \ge g(\text{parent}) \ge g(n_1)$, using $c \ge 0$ and the fact that every subsequently expanded parent was itself popped at or after $n_1$. Replacement only ever lowers a frontier node's $g$ — but never below the cost of the path that produced it, which by the same argument is $\ge g(n_1)$. Hence every node popped after $n_1$ has $g \ge g(n_1)$. $\blacksquare$
This is where non-negative costs are essential: a negative edge could produce a child cheaper than the node just popped, un-nesting the rings — which is why Dijkstra's algorithm (UCS by another name) fails on negative weights.
Claim. At the moment UCS pops a node $n$ with state $s$, $g(n)$ equals the cost of a cheapest path from the start to $s$.
Proof (by contradiction). Suppose some cheaper path $P^*$ to $s$ exists, with total cost $g^* < g(n)$.
Walk along $P^*$ from the start and let $u$ be the first state on it that is not in the explored set at this moment. Such a $u$ exists, because $s$ itself is not yet explored (we are popping it right now).
Case A: $u$ is the start state. Then the start was never expanded, which is impossible unless $n$ is the start node itself, in which case $g(n) = 0$ and nothing can be cheaper.
Case B: $u$ has a predecessor $u'$ on $P^*$, and by the choice of $u$, $u'$ is explored. So $u'$ was expanded at some earlier iteration, and at that moment $u$ was generated as a child with cost
$$g(u) \;\le\; \text{cost of the prefix of } P^* \text{ up to } u.$$(“$\le$” and not “$=$”, because $u'$ might itself have been reached by an even cheaper route, and because a later replacement can only lower $g(u)$ further.) Therefore $u$ is on the frontier right now with
$$g(u) \;\le\; \text{prefix cost} \;\le\; g^* \;<\; g(n),$$where the middle inequality holds because the remaining suffix of $P^*$ has non-negative cost.
But that contradicts the fact that $n$ was popped as the frontier's minimum: $u$ was sitting right there with a strictly smaller key. Hence no cheaper path exists and $g(n)$ is optimal. $\blacksquare$
Optimality. The first goal node popped has optimal $g$ by the theorem, and by Lemma 1 nothing popped later could be cheaper, so the returned plan is a cheapest plan. This uses goal-test-on-pop: if we returned on generation the theorem would not apply, because a generated node's $g$ need not be optimal yet.
Safety of the explored set. A state's $g$ is already optimal when it is popped, so rediscovering it later can never improve it — which is precisely why the pseudocode may ignore children already in the explored set. This is the step that breaks for A* with a merely admissible (non-consistent) heuristic, the subject of the next chapter.
Suppose a solution of finite cost $C^*$ exists. Every pop has $g \ge$ the previous pop's $g$ (Lemma 1), and a $k$-step path costs at least $k\varepsilon$, so only finitely many nodes have $g \le C^*$ — at most the states within depth $\lfloor C^*/\varepsilon \rfloor$. UCS can therefore make only finitely many pops with $g \le C^*$, and it cannot pop anything with $g > C^*$ while the goal's node (key $\le C^*$) is on the frontier. Hence the goal is popped within finitely many steps. $\blacksquare$
If $\varepsilon = 0$ is allowed the argument collapses: infinitely many nodes can share cost 0, and UCS can spin forever inside a zero-cost loop without raising the contour.
14. What's wrong with UCS — and what fixes it
Two complaints, both visible in Figure 4:
- It explores options in every “direction.” The contours are circles centred on the start, so UCS works as hard walking away from the goal as towards it: $g$ is blind to where the goal is.
- It has no information about goal location. Nothing in $\text{Actions}$, $\text{Result}$ or $c$ tells UCS which way the goal lies. That is what uninformed means.
The fix is a second number in the priority: keep $g(n)$, the backward cost paid, and add $h(n)$, an estimate of the forward cost still to pay. Order by $h$ alone and you get greedy best-first search (fast, not optimal); order by $f(n) = g(n) + h(n)$ and you get A*, whose contours stretch from circles into ellipses aimed at the goal, with optimality surviving as long as $h$ never overestimates.
UCS is A* with $h \equiv 0$: the frontier, the explored set, the replacement rule, goal-test-on-pop, the contour argument and even the shape of the optimality proof all carry over unchanged. Only the priority changes. Continue with Chapter 2: Heuristics, Greedy search and A*.
15. Recap and self-check
The Essentials box at the top of this page is the recap — read it once more, then take these two checks cold.
S-A-C-D: 7 and S-B-D: 5, and reports the answer S-A-C-D-G with cost 8. Name every error.S-B-D plus the 3 for $D \to G$ — but it was attached to the wrong path because the parent pointer was never updated. The root cause is the first error: keeping two nodes for state $D$. Fix the frontier and the path fixes itself. Cost of the path they actually wrote down: $1+2+4+3 = 10$, which doesn't match their own 8 — always sanity-check your answer by re-adding the edge costs along the path you report.