WEEK 2
CHAPTER 2

Search & A*

Three roads lead to Almaty. Four algorithms choose differently. Only some are guaranteed to find the cheapest.

Manga page: Karaganda bus station, 07:00. / Karaganda or Pavlodar first? I could just try every road... / WHOA / Too many! Let's pick a smart SEARCH STRATEGY.

Formulating the problem

Our course uses one teaching map for every algorithm (costs are invented classroom values, not real distances):

  • States: cities. A search state keeps only the details needed for planning. That simplification is called abstraction; weather and population are dropped.
  • Successor function: drive along a road to a connected city, with cost = road weight.
  • Start state: Astana. Goal test: state == Almaty.
  • Solution: a sequence of actions from START to GOAL. The optimal solution has the lowest total path cost.

State-space graph vs search tree

In the state-space graph each state appears once. A search tree is a "what-if" tree of plans: the root is the start, children are successors, and each tree node stands for a whole path. The same city can appear many times in the tree. Neither is usually built in full; we grow the tree only as needed.

The frontier (or fringe) holds the partial plans waiting to be explored. Every algorithm in this chapter follows the same loop, and they differ in exactly one thing: which frontier node to expand next.

function SEARCH(problem, strategy):
    frontier ← { path containing only START }
    loop:
        if frontier is empty: return failure
        path ← strategy.choose(frontier)        # ← the only difference!
        if GOAL-TEST(last state of path): return path
        for each successor s: add path + [s] to frontier

DFS

STACK · LIFO
Expand the deepest node first. Low memory. Not optimal; the result depends on neighbour order.

BFS

QUEUE · FIFO
Expand the shallowest node first. Finds the path with the fewest actions, which is optimal only if all steps cost the same.

UCS

PRIORITY · g(n)
Expand the node with the lowest cost so far g(n). Optimal with positive costs.

A*

PRIORITY · g+h
Expand the lowest f(n) = g(n) + h(n): cost so far plus an estimate of what remains.
TRY IT! Search visualiser: Astana → Almaty (graph from Practical 2)
unvisitedin frontier expandedfinal pathh = heuristic estimate to Almaty

Frontier (the next one to be chosen is highlighted):

Expanded order–
States expanded0
Path found–
Path cost–

Press Step to expand the first node.

WHAT TO NOTICE
  • UCS and A* both find the cheapest route (cost 12), but A* expands only 4 states while UCS expands 8. The heuristic saved half the work.
  • BFS happens to return the same route here, but only because the cheapest route also has the fewest roads. BFS never looks at costs.
  • Greedy best-first trusts h alone and returns Astana → Karaganda → Shymkent → Almaty with cost 13: fast, but not optimal.
  • Reverse Astana's neighbour order and run DFS: it now dives into Pavlodar first and returns a different, more expensive (13) route.

Worked example: A* by hand

Heuristic values from the practical: h(Astana)=10, h(Karaganda)=9, h(Pavlodar)=9, h(Balkhash)=5, h(Shymkent)=3, h(Semey)=6, h(Taldykorgan)=2, h(Almaty)=0.

StepExpandNew frontier entries (g + h = f)Frontier after step
0–Astana: 0 + 10 = 10Astana(10)
1AstanaKaraganda: 2 + 9 = 11; Pavlodar: 4 + 9 = 13Karaganda(11), Pavlodar(13)
2KaragandaBalkhash: 7 + 5 = 12; Shymkent: 10 + 3 = 13Balkhash(12), Pavlodar(13), Shymkent(13)
3BalkhashAlmaty: 12 + 0 = 12Almaty(12), Pavlodar(13), Shymkent(13)
4AlmatyGoal! Path Astana → Karaganda → Balkhash → Almaty, cost 2 + 5 + 5 = 12

Notice the goal test happens when a node is expanded, not when it is generated. If A* stopped as soon as it saw Almaty, a cheaper route still waiting in the frontier could be missed.

Heuristics: admissible and consistent

A heuristic h(n) estimates the remaining cost from n to the goal. It is extra problem knowledge, like a straight-line distance on a map.

admissible: 0 ≤ h(n) ≤ h*(n) (h* = true cheapest cost to goal) consistent: h(n) ≤ c(n, n') + h(n') for every edge n → n'

An admissible heuristic is optimistic: it never overestimates. Every consistent heuristic is also admissible. Check our table against the true remaining costs:

CityAstanaKaragandaPavlodarBalkhashShymkentSemeyTaldykorgan
h(n)10995362
true h*(n)121095362
h ≤ h*?✓✓✓✓✓✓✓

It is also consistent: e.g. Karaganda → Balkhash: 9 ≤ 5 + 5 ✓, and Astana → Karaganda: 10 ≤ 2 + 9 ✓ (all 9 edges pass).

Why A* is optimal (proof sketch from the lecture)

Let A be an optimal goal and B a suboptimal goal, both reachable, and h admissible. Some ancestor n of A is in the frontier while B waits there.

  1. f(n) = g(n) + h(n) ≤ g(A) = f(A), because h never overestimates the remaining cost.
  2. f(A) = g(A) < g(B) = f(B), because h = 0 at goals and B is suboptimal.
  3. So f(n) < f(B): every ancestor of A, and then A itself, is expanded before B. A* returns A.

Special cases: if h(n) = 0 everywhere, f = g and A* becomes UCS. If we use only h (ignore g), we get greedy best-first search, which can be fast but is not optimal. An overestimating heuristic may speed things up but loses the guarantee.

Comparing the strategies

b = branching factor, d = depth of the shallowest goal, m = maximum depth, C* = optimal cost, ε = smallest step cost.

AlgorithmFrontierComplete?Optimal?TimeSpace
BFSFIFO queueYes (finite b)Only if all step costs are equalO(bd)O(bd)
DFSLIFO stackNo in infinite spaces (yes in finite graphs with a visited set)NoO(bm)O(b·m)
Iterative deepeningStack, depth limit 1, 2, 3…YesIf step costs are equalO(bd)O(b·d)
UCSPriority queue on gYes (costs ≥ ε > 0)YesO(b1+⌊C*/ε⌋)same
A*Priority queue on g+hYes (finite b, costs ≥ ε > 0)Yes if h is admissible (tree search) / consistent (graph search)Depends on h qualityKeeps all generated nodes

Iterative deepening runs depth-limited DFS with limit 1, then 2, then 3… It combines DFS's small memory with BFS's preference for shallow solutions. The repeated work is cheap, because most nodes live at the deepest level anyway.

The code (Python, as in the practical)

import heapq
def astar(graph, h, start, goal):
    frontier = [(h[start], 0, start, [start])]      # (f, g, state, path)
    best_g = {start: 0}
    while frontier:
        f, g, node, path = heapq.heappop(frontier)  # smallest f first
        if g != best_g.get(node): continue          # stale entry
        if node == goal: return path, g               # goal test on EXPANSION
        for nxt, step in graph[node]:
            new_g = g + step
            if new_g < best_g.get(nxt, float("inf")):
                best_g[nxt] = new_g
                heapq.heappush(frontier, (new_g + h[nxt], new_g, nxt, path + [nxt]))
    return None, float("inf")

Delete + h[nxt] (use g only) and you have UCS. The search visualiser above runs this same logic in JavaScript.

REAL WORLD

Navigation apps, game characters finding paths, warehouse robots and network routing all rely on variants of these algorithms. Route planners on continent-sized road maps add pre-computed shortcuts, but the core idea of "expand the most promising partial route" is A*.

DID YOU KNOW?

A* was published in 1968 by Peter Hart, Nils Nilsson and Bertram Raphael for Shakey, one of the first mobile robots that could reason about its own actions.

MINI-QUIZ

1. Nodes: A has g=4, h=8; B has g=7, h=2. Which does A* expand first?

f(A) = 4 + 8 = 12 and f(B) = 7 + 2 = 9. A* is willing to pay more now if the estimated total looks better.

2. Does the path with fewer roads always have the lowest total cost?

Example: A–B (2), B–F (10) vs A–C (8), C–F (1). Both routes have 2 roads, but they cost 12 and 9. That's why BFS is optimal only when every step costs the same; UCS compares total costs.

3. If h(n) = 0 for every state, A* behaves like…

f = g + 0 = g, which is exactly UCS's priority.
Manga page: The cheapest route goes via Balkhash! / I heard there's a famous game champion there...
2