Skip to content
coden

State-space search: BFS, DFS, UCS and A* with animations

AI basics made visual: what a state space is, how BFS, DFS, UCS, greedy search and A* work, what a heuristic is and how it all connects to machine learning.

14 minutes Matěj Žalmánek 2026-10-15

Series AI basics Part 21 of 52
  1. Map of artificial intelligence: what is what and how it all fits together
  2. A brief history of artificial intelligence: from Turing to language models
  3. Data, model, training, inference: the basic terms on a single example
  4. How to read mathematical notation: symbols, indices, sums and quantifiers
  5. Sets and relations: the building blocks of definitions in AI
  6. Functions and mappings: why every AI model is a function
  7. Logic and proofs: the rules AI reasons by
  8. Graphs and trees: the structure that both search and neural networks stand on
  9. Linear algebra for machine learning: vectors, matrices and why AI mostly multiplies
  10. Derivatives, the gradient and the chain rule: how a model finds out which way to learn
  11. Gradient descent: how a model learns
  12. What probability is: from rolling dice to the Kolmogorov axioms
  13. Conditional probability and Bayes’ theorem: how to update your estimate when you learn something new
  14. Random variables and the cumulative distribution function: one function that tells you everything about a distribution
  15. Probability density and probability mass functions: why a continuous variable has zero probability at a point
  16. Expected value, variance and covariance: where a distribution lies, how wide it is and what goes with what
  17. Overview of probability distributions: Bernoulli, binomial, Poisson, uniform, normal and exponential
  18. Law of large numbers and central limit theorem: why averages behave so predictably
  19. Statistics and parameter estimation: maximum likelihood, MAP and confidence intervals
  20. Information theory: entropy, cross-entropy and KL divergence — where loss functions come from
  21. State-space search: BFS, DFS, UCS and A* with animations
  22. Games and adversaries: minimax, alpha-beta pruning and Monte Carlo tree search
  23. Constraint satisfaction: backtracking, constraint propagation and AC-3 on sudoku and a map of Czech regions
  24. Logic and knowledge systems: forward and backward chaining, resolution and expert systems
  25. Planning: STRIPS, the blocks world, heuristics from relaxation and planning in AI agents
  26. Local search and simulated annealing: a travelling salesman through the Czech regional capitals
  27. Genetic algorithms: population, crossover, mutation and selection on a backpack for a hike
  28. Evolution strategies and genetic programming: from the one-fifth rule to Kepler’s law
  29. Ant colony optimization: how a pheromone trail finds a short path
  30. Particle swarm optimisation: inertia, personal experience and the swarm’s experience
  31. Neuroevolution: evolving the weights and shape of neural networks, and the NEAT algorithm
  32. Fuzzy sets and membership functions: how to write down “somewhat warm”
  33. Fuzzy logic and probability: why “somewhat warm” does not mean “probably warm”
  34. Fuzzy logic: t-norms, implications and IF–THEN rules
  35. Fuzzy control: Mamdani and Sugeno from a fan to a pole on a cart
  36. Fuzzy clustering: the fuzzy c-means algorithm and points between clusters
  37. Neuro-fuzzy systems (ANFIS): fuzzy rules that learn from data
  38. Markov chains: the transition matrix, the stationary distribution and PageRank
  39. Hidden Markov models: the Viterbi algorithm and the dishonest casino
  40. Bayesian networks: why the grass is wet and what it means for the sprinkler
  41. The Kalman filter: how to estimate where a car is and how fast it goes from imprecise measurements
  42. Types of machine learning: supervised, unsupervised, semi-supervised and more
  43. Prompts, context and RAG: how to give a model the right information
  44. Variational autoencoders (VAE): new images from a continuous latent space
  45. Generative adversarial networks (GAN): the forger versus the detective
  46. Diffusion models: how pure noise becomes an image
  47. Markov decision processes: states, actions, rewards and the Bellman equations
  48. Q-learning and DQN: learning the value of actions, from a table to a neural network
  49. Policy gradient and PPO: learning the policy directly
  50. How an AI agent works: a language model in a loop with tools
  51. Programming with AI agents: the task, tests and review
  52. Evaluating and securing AI systems: how to measure and how to keep watch

Full series syllabus 52 of 83 topics published

What you will take away
  • How to formally write a task as finding a path in a graph of states.
  • How BFS, DFS, UCS, greedy best-first search and A* differ — and why at their core it is a single line of code.
  • What a heuristic is, when it is admissible and why with it A* finds the cheapest path with the least work.

Useful to know first: Map of artificial intelligence: what is what and how it all fits together, How to read mathematical notation: symbols, indices, sums and quantifiers

When people hear artificial intelligence today, most of them think of neural networks and large language models. But long before machines started learning from data, they could search. Navigating a map, a move in chess, a robot's plan in a warehouse — all of it is finding a path through a space of possibilities. State-space search is the first chapter of every AI textbook, and you will meet its ideas in machine learning too: heuristics, optimization, looking for the best solution.

You will find every concept here twice: first a precise mathematical definition, and right below it the same thing in plain words. Every animation is a real run of the algorithm — none of it is drawn by hand.

1. What a state space is

Before we search for anything, we have to describe the problem in a way a computer understands. That is what a search problem is for:

Definition — search problem

A search problem is a six-tuple

P = (S, s0, A, T, G, c)

that satisfies:

  1. S is a non-empty set; its elements are called states;
  2. s0 ∈ S; this state is called the initial state;
  3. A assigns to every state s ∈ S a finite set A(s) of actions applicable in state s; let 𝒟 = {(s, a) : s ∈ S, a ∈ A(s)} denote the set of all pairs “state, action applicable in it”;
  4. T : 𝒟 → S is the transition function; for (s, a) ∈ 𝒟 the state s′ = T(s, a) is a successor of state s (after action a);
  5. G ⊆ S is the set of goal states;
  6. c : 𝒟 → ℝ+ = (0, ∞) is the action cost: c(s, a) > 0 is the cost of applying action a in state s.

So the problem says exactly where we start, which moves are available in each state, where each move leads, how much it costs and when we are at the goal.

In plain words

A state is a “snapshot of the world” at one moment — where the little figure stands in the maze, how the tiles of a puzzle lie. An action is a move you can make. The transition function tells you where that move takes you. The goal is where you want to get, and the cost says how much each move costs you — time, fuel, steps.

Definition — path, solution, optimum

Let P = (S, s0, A, T, G, c) be a search problem and k ∈ ℕ0. A path of length k from the initial state is a sequence

π = (s0, a1, s1, a2, …, ak, sk)

such that for every i = 1, …, k we have ai ∈ A(si−1) and si = T(si−1, ai). (For k = 0 the path is just (s0).)

The cost of the path π is the sum of the costs of its actions:

g(π) = ∑i=1k c(si−1, ai).

A path π is a solution if and only if sk ∈ G. Let Π denote the set of all solutions. If Π ≠ ∅, the optimal cost is

C* = min {g(π) : π ∈ Π}

(if the minimum exists; in problems with finitely many states, like our maze, it always does) and a solution π is optimal if and only if g(π) = C*. An optimal solution is thus a solution that is not more expensive than any other solution.

In plain words

A path is a list of moves that follow one another. A solution is a path that reaches the goal. The optimal solution is the cheapest of them all — and that is usually the one we want to find.

States connected by actions form a directed graph: states are the vertices, actions the edges with weight c. Looking for a solution therefore means finding a path in the graph from s0 to some vertex in G. The classic example is the 8-puzzle: tiles 1–8 in a 3 × 3 frame and one empty space. A state is the arrangement of the tiles, an action moves the empty space, and the goal is the tiles in order:

12345678 up 13425678 right 12345678 down 12347568 left 12345678
One state of the 8-puzzle and its four successors

It looks innocent, but the 8-puzzle has 181,440 reachable states and its bigger sibling, the 15-puzzle, over ten trillion. That is why we never build the whole graph up front. The algorithm discovers it gradually — from the transition function it only ever computes the successors of the state it is standing on.

Example — where 181,440 comes from

Eight tiles and the empty space can be placed into nine cells in 9! = 9 · 8 · 7 · 6 · 5 · 4 · 3 · 2 · 1 = 362,880 ways. By sliding, though, you can reach only half of them: if you write the tiles down row by row (without the empty space), every move preserves whether that order contains an even or an odd number of “larger before smaller” pairs. A horizontal move does not change the order at all, a vertical move makes a tile jump over two others and changes the number of such pairs by 0 or by ±2. A state with the opposite parity therefore cannot be reached. That exactly half of the states are reachable (for the 15-puzzle with a slightly more complicated invariant that also involves the row of the empty space) was proved by W. W. Johnson and W. E. Story in 1879.

8-puzzle: 9!/2 = 362,880/2 = 181,440 states.
15-puzzle: 16!/2 = 20,922,789,888,000/2 = 10,461,394,944,000 ≈ 1.05 · 1013 states — over ten trillion.

2. The task: a path through a maze

We will show all the algorithms on the same task. We have a maze of 9 × 14 cells and want to get from the start to the goal as cheaply as possible. First, what each cell means:

  • Free cell — stepping onto it costs 1
  • Mud — you can walk through it, but stepping onto it costs 5
  • Wall — you cannot step onto it
  • S Start — the initial state, cell (0, 0)
  • G Goal — cell (8, 13)
  • Explored cell — the algorithm has expanded it
  • Path found — the result of the algorithm
S G 0 1 2 3 4 5 6 7 8 9 10 11 12 13 0 1 2 3 4 5 6 7 8
The maze from the task — the numbers along the edge are rows and columns
Formal problem statement

We write a cell as a pair (r, s) — row r ∈ {0, …, 8}, column s ∈ {0, …, 13}. Let Z be the set of walls and B the set of mud cells.

  • States: S = ({0, …, 8} × {0, …, 13}) ∖ Z — every cell except the walls.
  • Initial state: s0 = (0, 0). Goal: G = {(8, 13)}.
  • Moves: ↑ = (−1, 0), → = (0, 1), ↓ = (1, 0), ← = (0, −1). The actions applicable in state (r, s) ∈ S are those that lead to a cell inside the grid that is not a wall: A((r, s)) = {(Δr, Δs) ∈ {↑, →, ↓, ←} : (r + Δr, s + Δs) ∈ S}.
  • Transition: T((r, s), (Δr, Δs)) = (r + Δr, s + Δs), e.g. T((r, s), →) = (r, s + 1).
  • Cost: c(x, a) = 5 if T(x, a) ∈ B, otherwise c(x, a) = 1 — you pay for the cell you step onto.
  • Task: find a solution with the lowest cost C*.
In plain words

The little figure stands top left and wants to get to the bottom right. It only walks up, down, left and right, it cannot pass through a wall, and every step costs it 1 — except a step into the mud, which costs 5. We are looking for the path that costs it the least.

The mud lies on the most direct route on purpose. We will see that not every algorithm notices it: some count only steps, others the real cost.

3. The common skeleton of all the algorithms

All the algorithms in this article share the same skeleton. They differ in just one decision.

Definition — frontier, closed states, expansion

Let P = (S, s0, A, T, G, c) be a search problem. For a state s ∈ S, denote the set of its successors

N(s) = {T(s, a) : a ∈ A(s)}.

At every moment of its run a search algorithm maintains two sets of states:

  1. the frontier F ⊆ S — states the algorithm has already discovered (a path found from s0 leads to them) but not yet expanded; at the start F = {s0};
  2. the closed states E ⊆ S — states it has already expanded; at the start E = ∅.

To expand a state s ∈ F means to make these two changes (in this order):

E ← E ∪ {s},   F ← (F ∖ {s}) ∪ (N(s) ∖ E).

The expanded state thus moves from the frontier to the closed states, and those of its successors that have not been expanded yet are added to the frontier.

In plain words

The frontier is a list of places you know a path leads to, but you have not looked at yet. Closed states are places you have already been — thanks to them you do not walk in circles. Expanding a state means standing on it and looking around to see where you can go next.

frontier = [start]
closed = {}
while frontier is not empty:
    state = pick_from(frontier)    # ← this is where the algorithms differ
    if state is the goal: done
    add state to closed
    for each successor of state:
        if it is not closed yet: add it to the frontier

The whole difference between the algorithms lies in a single line: which state from the frontier we pick next. A queue, a stack, or a priority queue ordered by some number. Everything else rests on that.

4. Breadth-first search (BFS)

Definition — BFS

Input: a search problem P = (S, s0, A, T, G, c). Output: a solution (a path from s0 to some state in G), or the message “no solution exists”.

Breadth-first search (BFS) is the common skeleton above in which the frontier F is a queue (FIFO, first in — first out):

  1. F ← (s0), E ← ∅.
  2. If F is empty, stop: no solution exists.
  3. Remove from F the state s that entered it first.
  4. If s ∈ G, stop and return the path from s0 to s.
  5. Expand s: add it to E and append every successor s′ ∈ N(s) that is neither in E nor in F to the end of F. Continue with step 2.

Properties. Let d(s) denote the depth of state s — the smallest length of a path from s0 to s. BFS expands states in non-decreasing order of d(s), and therefore returns the solution with the fewest actions. Let b = maxs ∈ S |A(s)| be the branching factor (the largest number of successors of a state) and d the depth of the shallowest solution. Then BFS generates at most b + b2 + … + bd = O(bd) states and keeps just as many in memory.

In plain words

First come, first served — like a queue in a shop. You first explore everything one step from the start, then everything two steps away, then three. It spreads like a ripple on water, which is why the first path to reach the goal has the fewest steps. The catch: you have to remember the whole ripple, and it grows terribly fast.

A 1 B 2 C 3 D 4 E 5 F 6 G 7 H 8 I 9 J 10
Order of visits in BFS — layer by layer A → B → C → D → E → F → G → H → I → J

In the maze the ripple is easy to see — it spreads in all directions until it hits the goal:

S G
Breadth-first search cells explored: 75 steps: 25 path cost: 37

Notice, though, that the path goes straight through the mud. BFS counts actions, not cost — to it a step into mud is the same as a step on dry ground. The shortest path came out at 25 steps, but a cost of 37. BFS is optimal only when all actions cost the same.

Example — the cost of the BFS path and how fast the ripple grows

The BFS path has 25 steps, 3 of them into mud: g = 22 · 1 + 3 · 5 = 22 + 15 = 37.

The size of the ripple for branching factor b = 10: at depth d = 5 it is 10 + 100 + 1,000 + 10,000 + 100,000 = 111,110 states, at depth d = 10 already 11,111,111,110 ≈ 1.1 · 1010. If one state took 1 kB of memory, the ripple at depth 10 would need over 11 TB. Every further level multiplies that by ten.

from collections import deque

def bfs(start, goal, successors):
    frontier = deque([start])
    parent = {start: None}
    while frontier:
        state = frontier.popleft()      # queue: first in, first out
        if state == goal:
            return path(parent, goal)
        for nxt in successors(state):
            if nxt not in parent:
                parent[nxt] = state
                frontier.append(nxt)
    return None

def path(parent, state):
    result = []
    while state is not None:
        result.append(state)
        state = parent[state]
    return result[::-1]

5. Depth-first search (DFS)

Definition — DFS

Input: a search problem P = (S, s0, A, T, G, c). Output: a solution, or the message “no solution exists”.

Depth-first search (DFS) is the common skeleton above in which the frontier F is a stack (LIFO, last in — first out):

  1. F ← (s0), E ← ∅.
  2. If F is empty, stop: no solution exists.
  3. Remove from F the state s that entered it last (the top of the stack).
  4. If s ∈ G, stop and return the path from s0 to s.
  5. Expand s: add it to E and push every successor s′ ∈ N(s) ∖ E onto the top of F. Continue with step 2.

Properties. Let b be the branching factor and m the maximum depth of the state space (the largest length of a path without repeated states, possibly ∞). In the tree variant (without the set E; repeated states are checked only on the current branch) DFS keeps only the current branch and its siblings in memory: O(b · m) states. Time is O(bm) in the worst case. DFS is not optimal, and in an infinite space it is not even complete — it can keep descending forever.

In plain words

Like keeping your right hand on the wall in a maze: you pick one corridor and follow it all the way to the end. Only at a dead end do you go back to the last turning and try another. It needs little memory — you only remember the path you are walking right now. But you will find the shortest path only by chance.

A 1 B 2 D 3 H 4 I 5 E 6 C 7 F 8 G 9 J 10
Order of visits in DFS — depth first A → B → D → H → I → E → C → F → G → J

In the tree you can see it in the order: A, B, D, H, I — and only then E. Why, the frontier shows step by step. The skeleton is the same for both; they differ only in which end of the frontier they take from: BFS takes the oldest state from the left (the head of the queue), DFS the newest from the left (the top of the stack).

stepBFS expandsqueue afterwardsDFS expandsstack afterwards
1AB CAB C
2BC D EBD E C
3CD E F GDH I E C
4DE F G H IHI E C
5EF G H IIE C
6FG H IEC
7GH I JCF G
8HI JFG
9IJGJ
10J—J—

In the second step both add the children of node B, that is D and E. BFS appends them to the end of the queue behind C and takes C next, because it has waited longest. DFS pushes them onto the top of the stack above C and takes D, because it came last — and the children H, I then get ahead even of E and C. The queue grows to as many as five nodes (E F G H I), the stack to at most four (H I E C): in BFS the frontier holds a whole level, in DFS only one branch and the siblings of its nodes. On such a small tree the difference is one node, but it grows with every level. In the maze:

S G
Depth-first search cells explored: 32 steps: 31 path cost: 59

Here DFS got lucky and reached the goal without exploring many cells. But the path is needlessly long — 31 steps instead of 25 and a cost of 59.

Definition — iterative deepening (IDDFS)

Input: a search problem P = (S, s0, A, T, G, c). Output: a solution with the fewest actions (if any solution exists).

For ℓ ∈ ℕ0, let DLS(ℓ) (depth-limited search) denote depth-first search that does not expand states at depth ℓ or more — so it does not examine paths longer than ℓ. Iterative deepening (IDDFS):

  1. ℓ ← 0.
  2. Run DLS(ℓ).
  3. If DLS(ℓ) found a solution, stop and return it.
  4. ℓ ← ℓ + 1 and continue with step 2.

Stopping condition: the first ℓ for which DLS(ℓ) finds a solution; that is exactly the depth d of the shallowest solution. (If no solution exists and the space is infinite, the algorithm does not terminate.)

Properties. Let b be the branching factor and d the depth of the shallowest solution. States at depth i ∈ {1, …, d} are generated (d − i + 1) times, in total ∑i=1d (d − i + 1) · bi = O(bd) states — the same as BFS, but with memory of only O(b · d).

In plain words

You run DFS but forbid it to go more than one step deep. Found nothing? Then two steps, then three… You go through the upper levels again and again, but most states always lie in the deepest one, so the repetition costs almost nothing. You get the shortest path like BFS and the small memory of DFS.

NIDDFS = ∑i=1d (d − i + 1) · bi

NIDDFS
how many states the whole iterative deepening generates — this is how its time is measured
∑i=1d
we sum level by level of the tree, from depth 1 down to the depth of the solution d — it never goes deeper
(d − i + 1)
how many times level i is gone through: once in every round with limit ℓ = i, i + 1, …, d — the shallow levels are repeated most often
bi
how many states there are at level i — it grows exponentially with depth, so the deepest level, gone through only once, outweighs everything else
For b = 10, d = 5: NIDDFS = 5 · 10 + 4 · 100 + 3 · 1,000 + 2 · 10,000 + 1 · 100,000 = 50 + 400 + 3,000 + 20,000 + 100,000 = 123,450. BFS generates 111,110 states, so the repetition costs only 123,450/111,110 − 1 ≈ 11% extra (an example from the textbook by Russell and Norvig, Artificial Intelligence: A Modern Approach).
def iddfs(start, is_goal, successors):
    limit = 0
    while True:                            # ℓ = 0, 1, 2, …
        path = dls(start, is_goal, successors, limit)
        if path is not None:
            return path
        limit += 1

def dls(state, is_goal, successors, limit):   # DFS with a depth limit
    if is_goal(state):
        return [state]
    if limit == 0:
        return None                        # we must not go deeper
    for nxt in successors(state):
        path = dls(nxt, is_goal, successors, limit - 1)
        if path is not None:
            return [state] + path
    return None

6. Uniform-cost search (UCS)

Definition — UCS

Input: a search problem P = (S, s0, A, T, G, c). Output: a solution with the lowest cost, or the message “no solution exists”.

During the run the algorithm assigns to every discovered state n ∈ S a number g(n) — the cost of the cheapest path from s0 to n found so far. The frontier F is a priority queue ordered by g. Uniform-cost search (UCS):

  1. F ← {s0}, g(s0) ← 0, E ← ∅.
  2. If F = ∅, stop: no solution exists.
  3. Remove from F the state n with the lowest g(n).
  4. If n ∈ G, stop and return the path found to n (its cost is g(n)).
  5. Add n to E. For every action a ∈ A(n) and n′ = T(n, a) ∉ E: if n′ ∉ F, or g(n) + c(n, a) < g(n′), set g(n′) ← g(n) + c(n, a), remember n as the predecessor of n′ and insert n′ into F. Continue with step 2.

Properties. Suppose there is an ε > 0 such that c(s, a) ≥ ε for all s ∈ S, a ∈ A(s). Then UCS expands states in non-decreasing order of g, and is therefore complete and optimal. The number of expanded states is O(b1 + ⌊C*/ε⌋), where b is the branching factor and ⌊x⌋ the floor of the number x.

In plain words

Instead of steps, you count money. You always continue from the place you have so far reached most cheaply. When you get to the goal, you know for sure it cannot be done cheaper — you have already explored everything cheaper. On any graph with positive costs this is what the well-known Dijkstra's algorithm does; UCS is its version that stops at the goal.

S G
Uniform-cost search cells explored: 49 steps: 25 path cost: 25

UCS went around the mud. It found a path with the same number of steps as BFS, but with a cost of 25 instead of 37 — and it is guaranteed to be the cheapest, so C* = 25. The price it pays is searching blindly in every direction: it does not know where the goal is, so it spreads away from it too.

Example — in what order UCS expands and what the complexity estimate says

The first ten expansions with their value of g: (0, 0) g = 0; (0, 1) g = 1; (1, 0) g = 1; (0, 2) g = 2; (2, 0) g = 2; (0, 3) g = 3; (3, 0) g = 3; (1, 3) g = 4; (4, 0) g = 4; (2, 3) g = 5. Until it runs into mud, UCS expands just like BFS, because every step costs 1 and g is simply the depth. Mud then “pushes back” a state by 5 instead of 1 — UCS looks into the mud only once it has used up everything cheaper.

The path found: 25 steps on dry ground, C* = 25 · 1 = 25. The smallest action cost is ε = 1 and the branching factor b = 4, so the upper bound is b1 + ⌊C*/ε⌋ = 426 ≈ 4.5 · 1015 states. In reality UCS expanded 49 cells: the bound assumes an infinite tree where nothing repeats, whereas the maze has only 83 passable cells and closed states are not expanded again.

7. Heuristics: a guess at the way to the goal

The algorithms above are uninformed — all they know about the goal is that they will recognise it once they step on it. Informed algorithms also get an estimate of how much is left.

Definition — heuristic

Let P = (S, s0, A, T, G, c) be a search problem. For a state n ∈ S, let h*(n) ∈ ℝ≥0 ∪ {∞} denote the lowest cost (more precisely the infimum of the costs) of paths from state n to some goal state (a path is defined as above, it just starts in n instead of s0; if no path leads from n to G, then h*(n) = ∞). A heuristic is a function

h : S → ℝ≥0  such that  ∀n ∈ G : h(n) = 0.

We read the value h(n) as an estimate of h*(n). A heuristic h is

  1. admissible if and only if ∀n ∈ S : h(n) ≤ h*(n);
  2. consistent (monotone) if and only if ∀n ∈ S ∀a ∈ A(n) : h(n) ≤ c(n, a) + h(T(n, a)).

An admissible heuristic thus never overestimates the real remaining cost; a consistent heuristic never drops after a step by more than that step cost. Every consistent heuristic is admissible (theorem below).

In plain words

A heuristic is an educated guess — like knowing in a foreign city that the station is “somewhere to the north”. Admissible means optimistic: it never claims it is further than it really is. Consistent means the guess does not contradict itself after one step — it cannot improve by more than what you paid for that step.

h(n) ≤ c(n, a) + h(n′)

h(n)
what the heuristic promises now, in state n — this promise must not be exaggerated
c(n, a)
the real cost of one step to the successor; for that step we really do pay
h(n′)
the estimate of the rest from the successor. If the sum on the right were smaller than h(n), a path through n′ would be cheaper than h(n) claims, and one of the two estimates would be nonsense
The triangle inequality: the direct estimate from n must not be worse than “one step and the estimate from there”.
Theorem — a consistent heuristic is admissible

Let P = (S, s0, A, T, G, c) be a search problem and h : S → ℝ≥0 a consistent heuristic. Then h is admissible, i.e. h(n) ≤ h*(n) for every n ∈ S.

Proof. Let n ∈ S. If h*(n) = ∞, the inequality holds trivially. Otherwise take any path from n to a goal: n = n0, n1, …, nk ∈ G, where ni = T(ni−1, ai) and ci = c(ni−1, ai) for i = 1, …, k. We apply consistency k times in a row:

h(n0) ≤ c1 + h(n1) ≤ c1 + c2 + h(n2) ≤ … ≤ c1 + … + ck + h(nk).

At the goal h(nk) = 0, so h(n) is at most the cost of this path. That holds for every path from n to a goal, hence also for the cheapest one, whose cost is h*(n). Therefore h(n) ≤ h*(n). ∎

The converse does not hold: an admissible heuristic need not be consistent.

In our maze the obvious choice is the Manhattan distance — the number of cells horizontally plus vertically, as if you were walking the right-angled streets of Manhattan:

h((r, s)) = |r − 8| + |s − 13|

h((r, s))
the estimate of the remaining cost from the cell in row r and column s
|r − 8|
how many rows are left to the goal (8, 13). The absolute value is there because the distance is counted the same whether the goal is above or below us; a vertical move removes at most one of them
|s − 13|
how many columns are left. We add them because you cannot move diagonally: every row and every column costs at least one separate move
From the start: h((0, 0)) = |0 − 8| + |0 − 13| = 8 + 13 = 21 ≤ C* = 25. The heuristic is consistent (and therefore admissible): one action changes h by at most 1 and costs at least 1.
In plain words

You count how many cells the goal is down and how many to the right, and add them up. Walls and mud can only make the path longer or more expensive, never shorter — so the estimate is always optimistic, exactly as it should be.

8. Greedy best-first search

Definition — greedy search

Input: a search problem P = (S, s0, A, T, G, c) and a heuristic h : S → ℝ≥0. Output: a solution, or the message “no solution exists”.

Greedy best-first search is the common skeleton above in which the frontier F is a priority queue ordered by

f(n) = h(n):

  1. F ← {s0}, E ← ∅.
  2. If F = ∅, stop: no solution exists.
  3. Remove from F the state n with the lowest h(n) — the state estimated to be closest to the goal.
  4. If n ∈ G, stop and return the path from s0 to n.
  5. Expand n: add it to E and insert every successor n′ ∈ N(n) that is neither in E nor in F into F. Continue with step 2.

The cost so far, g(n), is not taken into account. Properties: greedy search is not optimal; with repeated-state checking (the set E) it is complete in a finite space.

In plain words

You always go where the goal looks closest and do not care at all how much the path has cost you so far. It heads in the right direction fast, but it is easily fooled — for instance by mud that lies “in the right direction”.

S G
Greedy best-first search cells explored: 33 steps: 31 path cost: 59

Greedy search explores few cells, but it goes where things look closer and ends up on a long, expensive path — 31 steps, cost 59.

Example — the first ten steps of greedy search

Expanded cells with their value of h: (0, 0) 21 → (0, 1) 20 → (0, 2) 19 → (0, 3) 18 → (1, 3) 17 → (2, 3) 16 → (2, 2) 17 → (3, 2) 16 → (4, 2) 15 → (4, 3) 14.

Greedy search heads straight right and down as long as h decreases. In the seventh step h briefly rises (from 16 to 17), because from (2, 3) no path leads further right or down (walls) and there is no cell with a smaller h left in the frontier. Two steps later it steps into the mud at (4, 2), because that way h decreases again. It does not see the cost of 5 for a step into the mud at all: the priority is only h, no g.

9. A*: the best of both worlds

Definition — A*

Input: a search problem P = (S, s0, A, T, G, c) and a heuristic h : S → ℝ≥0. Output: a solution, or the message “no solution exists”.

A* is UCS (steps 1–5 of its definition above, including maintaining g) in which the frontier F is a priority queue ordered by

f(n) = g(n) + h(n)

instead of by g(n): in step 3 the state with the lowest f(n) is removed from F. The value f(n) is an estimate of the cost of the whole path from the start through n to the goal. The algorithm stops as soon as it removes a goal state from F (or when F is empty).

Theorem. Let h be a consistent heuristic and let action costs be ≥ ε for some ε > 0. Then the values of f along every path are non-decreasing, A* expands states in non-decreasing order of f, and the first goal state expanded lies on an optimal path with cost C*. In doing so it expands every state with f(n) < C* and none with f(n) > C*.

For h ≡ 0, A* is the same as UCS; without g it would be greedy search.

In plain words

For every place, A* adds up how much you have already paid and how much you will pay, by its estimate, from there. Thanks to the first part it does not fall for an expensive shortcut, thanks to the second it does not waste time on paths leading away from the goal. When the estimate is optimistic, it always finds the cheapest path — and explores only what it has to along the way.

f(n) = g(n) + h(n)

f(n)
the priority in the frontier: an estimate of the cost of the whole solution that goes through n. The smallest is expanded, because it is the most promising candidate for the optimum
g(n)
how much the path from the start to n has really cost. Without it A* would not know about the mud it went through and would behave like greedy search
h(n)
the estimate of how much is still left to the goal. Without it A* would not know where the goal is and would spread in all directions like UCS
From the start: f((0, 0)) = 0 + 21 = 21. On cell (6, 13), two steps before the goal: f((6, 13)) = 23 + 2 = 25 = C*.
Theorem — with a consistent heuristic the value of f does not decrease along a path

Let h be a consistent heuristic, f = g + h, n ∈ S, a ∈ A(n), n′ = T(n, a), and let the path to n′ go through n, so that g(n′) = g(n) + c(n, a). Then f(n′) ≥ f(n).

Proof. f(n′) = g(n) + c(n, a) + h(n′) ≥ g(n) + h(n) = f(n),

where the inequality is exactly the consistency h(n) ≤ c(n, a) + h(n′). ∎

From this it follows (here only as a sketch) that A* expands states in non-decreasing order of f: if it later expanded a state with a smaller f, the frontier would have to contain on the way to it a state with an even smaller f, which should have come first. So when the goal is expanded for the first time (where h = 0 and f = g), no cheaper path remains.

S G
A* with Manhattan distance cells explored: 37 steps: 25 path cost: 25

How A* proceeds is shown by the table of the first steps. For every expanded cell it gives g, h and f, and on the right the frontier after the expansion in the order in which the queue hands it out (the number after a cell is its f; on a tie, whatever was added earlier goes first; colours as in the formula above):

stepexpandsghffrontier afterwards
1(0, 0)02121(0, 1) 21, (1, 0) 21
2(0, 1)12021(1, 0) 21, (0, 2) 21
3(1, 0)12021(0, 2) 21, (2, 0) 21
4(0, 2)21921(2, 0) 21, (0, 3) 21
5(2, 0)21921(0, 3) 21, (3, 0) 21
6(0, 3)31821(3, 0) 21, (1, 3) 21
……
23(8, 9)17421(2, 2) 23, (8, 1) 23, (7, 5) 23, (7, 9) 23, (6, 3) 25, (5, 2) 27
24(2, 2)61723(8, 1) 23, (7, 5) 23, (7, 9) 23, (3, 2) 23, (6, 3) 25, (5, 2) 27
25(8, 1)111223(7, 5) 23, (7, 9) 23, (3, 2) 23, (6, 3) 25, (8, 0) 25, (5, 2) 27

The first 23 steps have f = 21: A* walks along cells where every step brings it closer to the goal, so whatever is added to g is taken away from h. At cell (8, 9) the corridor ends in a wall, and only now do the cells with f = 23 get their turn. A step on dry ground that moves away from the goal raises g by 1 and h by 1 as well, so f by 2; going around the wall therefore raises f in twos. In total it expanded 23 cells with f = 21, 5 cells with f = 23, 9 cells with f = 25. Exactly as the theorem says: it had to expand all 28 cells with f < C* = 25, of the cells with f = 25 only as many as it took to hit the goal, and none with f > 25.

The same cost of 25 as UCS, but A* explored 37 cells instead of 49. In a small maze that is a difference of a few cells; on a map with millions of intersections it is the difference between an instant answer and a minute of waiting. That is why you will find A* (and its variants) in navigation, in games and in robot motion planning.

import heapq

def a_star(start, goal, successors, h):
    frontier = [(h(start), 0, start)]     # (f, g, state)
    g = {start: 0}
    parent = {start: None}
    while frontier:
        _, cost, state = heapq.heappop(frontier)
        if state == goal:
            return path(parent, goal), cost
        if cost > g[state]:
            continue                       # stale entry, we already know a cheaper one
        for nxt, step in successors(state):
            new = cost + step
            if new < g.get(nxt, float("inf")):
                g[nxt] = new
                parent[nxt] = state
                heapq.heappush(frontier, (new + h(nxt), new, nxt))
    return None

10. Comparing the algorithms

Definition — properties of an algorithm

Let 𝒜 be an algorithm that receives a search problem P as input and returns a solution, or the message “no solution exists”. The algorithm 𝒜 is

  1. complete if and only if for every problem P that has at least one solution (Π ≠ ∅), 𝒜 stops after a finite number of steps and returns a solution π ∈ Π;
  2. optimal if and only if for every problem P with Π ≠ ∅ it returns a solution π with cost g(π) = C*.

The parameters that time and memory depend on (for a problem P = (S, s0, A, T, G, c)):

  1. b = maxs ∈ S |A(s)| — the branching factor;
  2. d — the depth of the shallowest solution (the smallest length of a path from s0 to G);
  3. m — the maximum depth of the space (the largest length of a path from s0 without repeated states, possibly ∞);
  4. C* — the optimal cost, and ε > 0 — a lower bound on the action costs.

For functions u, v ≥ 0 of these parameters we write u = O(v) if and only if there exist constants K > 0 and x0 such that u(x) ≤ K · v(x) for all x ≥ x0. O(·) thus states how time (the number of generated states) or memory (the number of states held at once) grow at most with these parameters, up to a constant factor.

In plain words

Complete = if a path exists, it finds it. Optimal = it finds the cheapest one. O(bd) means “with every extra step in depth, the work multiplies” — which is why memory matters so much.

AlgorithmCompleteOptimalTimeMemoryIn our maze
BFSyeswith equal costsO(bd)O(bd)75 cells, cost 37
DFSno*noO(bm)O(b·m)32 cells, cost 59
IDDFSyeswith equal costsO(bd)O(b·d)—
UCSyesyesO(b1+⌊C*/ε⌋)O(b1+⌊C*/ε⌋)49 cells, cost 25
Greedyno*noO(bm)O(bm)33 cells, cost 59
A*yesyes**depends on heuristicdepends on heuristic37 cells, cost 25

* In a finite space with repeated-state checking they always find a solution. ** With a consistent heuristic.

11. Local search: when the path does not matter

In some problems we do not care about the path — we only want a good final state. A class timetable, the placement of components on a circuit board, parameter tuning.

Definition — local search

Let S be a non-empty set of states, f : S → ℝ a function (the objective function) and N : S → 2S a map that assigns to every state s ∈ S a finite non-empty set N(s) ⊆ S ∖ {s} (the neighbourhood of state s; its elements are neighbours). The local search problem is to find a state

s* ∈ argmaxs ∈ S f(s) = {s ∈ S : ∀s′ ∈ S : f(s) ≥ f(s′)},

or at least a state with as large a value of f as possible. A state s ∈ S is

  1. a local maximum if and only if ∀s′ ∈ N(s) : f(s) ≥ f(s′);
  2. a global maximum if and only if ∀s′ ∈ S : f(s) ≥ f(s′).

Every global maximum is also a local one; the converse does not hold.

Hill climbing. Input: S, f, N as above and an initial state s ∈ S. Output: a local maximum.

  1. Choose s′ ∈ argmaxt ∈ N(s) f(t) (the best neighbour).
  2. If f(s′) ≤ f(s), stop and return s — it is a local maximum.
  3. Otherwise s ← s′ and continue with step 1.
In plain words

The objective function says how good a solution is — the higher, the better. The neighbourhood is the solutions you get by a small change. The climber stands on a hill in fog, sees only a little around them and always goes where it climbs. When everything around goes down, they stop — even though a much higher mountain may stand nearby.

local maximum global maximum state s (position 0–1) objective f(s) (unitless)
Hill climbing gets stuck on a local peak
def hill_climbing(s, neighbours, f):
    while True:
        best = max(neighbours(s), key=f)   # the highest neighbour
        if f(best) <= f(s):
            return s                       # nothing around is higher: a peak (maybe only a local one)
        s = best
Example — the climber from the figure step by step

A state is a number s ∈ [0, 1], the neighbourhood is the two points 0.02 to the left and to the right: N(s) = {s − 0.02, s + 0.02}. The landscape has two peaks (a local one at s = 0.25, the global one at 0.72) and the climber starts from s = 0.05.

stepsf(s − 0.02)f(s)f(s + 0.02)
00.050.0600.0700.087
10.070.0700.0870.115
20.090.0870.1150.157
30.110.1150.1570.216
40.130.1570.2160.289
50.150.2160.2890.373
60.170.2890.3730.457
70.190.3730.4570.531
80.210.4570.5310.582
90.230.5310.5820.600
100.250.5820.6000.582

In every row the right neighbour is the highest, so the climber goes right. In the last row both neighbours (0.582) are lower than f(0.25) = 0.600 and the algorithm stops — at a local maximum, even though the global maximum has f(0.72) = 0.950.

Against getting stuck you can use random restarts (letting the climber start from different places), genetic algorithms, which cross over and mutate a whole population of solutions, or simulated annealing:

Definition — simulated annealing

Input: S, f, N as in local search, an initial state s ∈ S, an initial temperature T0 > 0, a cooling coefficient q ∈ (0, 1) and a minimum temperature Tmin ∈ (0, T0). Output: a state s ∈ S.

  1. T ← T0.
  2. If T ≤ Tmin, stop and return s.
  3. Pick a neighbour s′ ∈ N(s) at random (uniformly) and compute Δ = f(s′) − f(s).
  4. If Δ > 0, set s ← s′. If Δ ≤ 0, set s ← s′ with probability exp(Δ/T), otherwise keep s.
  5. T ← q · T and continue with step 2.

The “temperature” T thus gradually falls to zero, and with it the willingness to accept a worse neighbour. (Cooling T ← q · T is the most common choice; other schedules are used too.)

In plain words

At the start the climber now and then allows themselves to go downhill too — that is how they get from a small peak to a bigger one. The longer they search, the more careful they get, until in the end they only climb.

P(accept s′) = exp(Δ/T), if Δ ≤ 0

P(accept s′)
the probability that a worse neighbour is accepted anyway — thanks to it the climber can get down from a local peak
exp
the exponential exp(x) = ex turns any negative number into a value between 0 and 1, i.e. a valid probability; borrowed from physics, where the same form (the Boltzmann factor) says how probable a state with higher energy is
Δ = f(s′) − f(s)
how much worse the neighbour is (a negative number). The worse it is, the smaller the chance: a small deterioration is accepted easily, a big drop almost never
T
the temperature — how willing we are to take risks. A high T wipes out the differences (the exponent is close to 0, P close to 1), a low one magnifies them
For a deterioration of Δ = −0.1: at T = 1, P = exp(−0.1/1) = e−0.1 ≈ 0.905, at T = 0.1, P = exp(−0.1/0.1) = e−1 ≈ 0.368, and at T = 0.01 already P = exp(−0.1/0.01) = e−10 ≈ 0.000045 — the climber almost only climbs.
def annealing(s, neighbour, f, T=1.0, cooling=0.995, T_min=1e-3):
    while T > T_min:
        s2 = neighbour(s)                  # a random neighbour, not the best one
        delta = f(s2) - f(s)
        if delta > 0 or random() < exp(delta / T):
            s = s2                         # better always, worse only sometimes
        T *= cooling                       # the temperature slowly falls
    return s

And this is where the circle closes with machine learning. Training a neural network is local search in the space of its weights θ: gradient descent repeats θ ← θ − η ∇L(θ) — a step of size η in the direction in which the loss function L falls fastest. It is hill climbing turned upside down, looking not for the highest peak but for the lowest valley — and it struggles with the same problems: local minima and plateaus.

12. Summary

  • A search problem is a six-tuple (S, s0, A, T, G, c). A solution is a path in the graph of states, the optimal solution the cheapest one.
  • The algorithms differ only in which state they pick from the frontier: a queue (BFS), a stack (DFS), the lowest g (UCS), the lowest h (greedy), the lowest g + h (A*).
  • BFS finds the fewest steps, UCS the lowest cost, A* the lowest cost while exploring the least — as long as it has a consistent heuristic.
  • When the result matters rather than the path, local search takes over — and from there it is only a step to gradient descent and machine learning.

Want to get a feel for the algorithms? Have an AI agent write your own visualisation — and then look in the code for that single line that sets them apart.

Do you see it differently? Write to support@coden-app.com.

An Agent on your computer, driven from your phone