🧠 AI Foundations · Lecture 5 of 24

Uninformed Search: BFS, DFS, Uniform-Cost and Iterative Deepening

Many AI problems reduce to finding a path in a huge graph. We formalise search problems and analyse the classic blind strategies for completeness, optimality, time and space.

Suppose you are in Dhaka and want to reach Chattogram using a road map, but you have no idea which direction Chattogram lies. You can only look at which cities are connected. How do you systematically find a route? This is uninformed (or blind) search, and it is the foundation on which all smarter search algorithms are built.

Formulating a search problem#

A search problem consists of:

  1. A set of states $S$ and an initial state $s_0$.
  2. A set of actions $A(s)$ available in each state.
  3. A transition model $\text{Result}(s, a)$ giving the next state.
  4. A goal test $\text{IsGoal}(s)$.
  5. An action cost function $c(s, a, s')$.

A solution is a sequence of actions from $s_0$ to a goal; an optimal solution has the lowest total cost. The states and transitions implicitly define a graph — often astronomically large (the 15-puzzle has about $10^{13}$ reachable states), so we generate it lazily.

The general algorithm#

All search algorithms maintain a frontier of nodes waiting to be expanded and (in graph search) a reached set to avoid repeated states. They differ only in which node to expand next.

python
from collections import deque
import heapq

def breadth_first_search(start, goal, neighbors):
    frontier = deque([start])
    parent = {start: None}
    while frontier:
        s = frontier.popleft()
        if s == goal:
            return reconstruct(parent, s)
        for t in neighbors(s):
            if t not in parent:          # early goal test variant is also common
                parent[t] = s
                frontier.append(t)
    return None

def uniform_cost_search(start, goal, neighbors_with_cost):
    frontier = [(0, start)]
    best = {start: 0}
    parent = {start: None}
    while frontier:
        g, s = heapq.heappop(frontier)
        if s == goal:
            return g, reconstruct(parent, s)
        if g > best[s]:
            continue                      # stale queue entry
        for t, c in neighbors_with_cost(s):
            if g + c < best.get(t, float("inf")):
                best[t] = g + c
                parent[t] = s
                heapq.heappush(frontier, (g + c, t))
    return None

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

Measuring search algorithms#

We judge a strategy on four criteria, using $b$ = branching factor, $d$ = depth of the shallowest goal, $m$ = maximum depth, $C^*$ = optimal cost, and $\epsilon$ = the minimum action cost.

StrategyComplete?Optimal?TimeSpace
Breadth-first (BFS)Yes (finite $b$)Yes if costs equal$O(b^d)$$O(b^d)$
Uniform-cost (UCS)Yes if $\epsilon > 0$Yes$O(b^{1+\lfloor C^*/\epsilon \rfloor})$same
Depth-first (DFS)No (infinite paths)No$O(b^m)$$O(bm)$
Depth-limitedNoNo$O(b^\ell)$$O(b\ell)$
Iterative deepening (IDS)YesYes if costs equal$O(b^d)$$O(bd)$
BidirectionalYesYes (with care)$O(b^{d/2})$$O(b^{d/2})$

Expands the shallowest node first using a FIFO queue. It finds the shortest path in number of steps. Its weakness is memory: with $b = 10$ and $d = 10$, BFS stores roughly $10^{10}$ nodes — far beyond typical RAM.

Expands the node with the smallest path cost $g(n)$ using a priority queue. It is exactly Dijkstra's algorithm, adapted to stop at the goal. It is optimal for any non-negative costs.

Expands the deepest node first (a LIFO stack or recursion). Its memory is only linear in depth, which is its great virtue, but it can wander down infinite branches and returns whatever solution it finds first.

Runs depth-limited DFS with limits $0, 1, 2, \dots$ until a goal is found. It seems wasteful to regenerate the upper levels repeatedly, but the total cost is dominated by the last level:

$$ N_{IDS} = (d)b + (d-1)b^2 + \dots + (1)b^d = O(b^d) $$

For $b = 10, d = 5$, IDS generates 123,450 nodes versus 111,110 for BFS — only about 11% more — while using linear memory.

Searches forward from the start and backward from the goal simultaneously, stopping when the frontiers meet. Since $2b^{d/2} \ll b^d$, the savings are enormous, but you must be able to compute predecessors and the goal must be explicit.

A worked example#

Consider the graph A–B (1), A–C (5), B–C (1), C–D (1). From A to D:

  • BFS finds A→C→D (2 steps, cost 6).
  • UCS finds A→B→C→D (3 steps, cost 3) — the cheaper route.

This illustrates the key lesson: BFS optimises the number of steps; UCS optimises total cost.

JA
Written by

Janin A Apurba

B.Sc. in CSE, AUST · Advanced ICT Officer, CNRS-UNHCR. Teaching AI, ML and Deep Learning to the next generation of engineers and researchers.

Keep learning

Related lectures

🧠 AI Foundations

Informed Search: Greedy Best-First and A* with Admissible Heuristics

Knowledge about where the goal lies transforms search. We derive A*, prove its optimality with admissible heuristics, and learn how to invent good heuristics by relaxing problems.

Intermediate⏱ 5 min#006
🧠 AI Foundations

Adversarial Search: Minimax and Alpha–Beta Pruning

When an opponent is trying to defeat you, search must account for their choices. We derive minimax, prove alpha–beta pruning correct, and see how real game engines evaluate positions.

Intermediate⏱ 5 min#007
🧠 AI Foundations

Local Search: Hill Climbing, Simulated Annealing and Beam Search

When only the final configuration matters, we can abandon paths and move through the space of complete solutions. Local search is simple, memory-light and the conceptual ancestor of gradient descent.

Beginner⏱ 5 min#017