Interactive companion · Chapter 3 · Blind search algorithms

Blind Search Lab

Blind (uninformed) search explores a graph by rule alone: level by level, branch by branch, or cheapest first.

Based on Chapter 3, “Blind search algorithms,” in Alaa Khamis, Optimization Algorithms: AI techniques for design, planning, and control problems, Manning Publications, 2024 (ISBN 978-1633438835).

Searching the road network around the University of Toronto’s St. George campus.

Part 1 · Section 3.1

Graphs

A graph G = (V, E) is a set of vertices joined by edges. Pick a graph type to see how it looks.

Section 3.1 of the book builds each type in Listings 3.1 to 3.6 and turns web, social and road networks into graphs.

Graph types (Listings 3.1 to 3.6)

Part 3 · Section 3.3

Graph traversal: BFS and DFS

BFS explores layer by layer and finds the fewest-edge path at a high memory cost; DFS dives deep and backtracks, using little memory but possibly wandering.

Section 3.3 of the book gives both as pseudocode (Algorithms 3.1 and 3.2) and adds depth-limited and iterative deepening search.

The 8-puzzle lab

Click a tile next to the blank to slide it, or load the book’s board, then search with BFS or DFS and play the solution.

Section 3.3 of the book formulates the 8-puzzle, explains which boards are solvable, and solves it in Listings 3.9 and 3.10.

Solve the 8-puzzle blindly

Goal: blank top left, then 1 to 8

Robot path planning

Step through the search and watch the frontier grow; click cells to add or remove obstacles.

Figures 3.13 to 3.25 in the book trace BFS and DFS on this grid move by move.

Path planning on a grid

Click to edit obstacles
startdestinationexplored (number = order)in the frontierroute
Frontier

Complexity: will it scale?

Big O notation describes how work grows with input size. Move the sliders to see how long each complexity class, and BFS, would take.

Section 3.3 of the book derives the time and memory of BFS and DFS and compares them in Tables 3.3 to 3.5.

How long would it take?

Part 4 · Section 3.4

Shortest path algorithms

Dijkstra’s algorithm finds shortest paths with non-negative costs by repeatedly settling the closest unsettled node and relaxing its edges.

Dijkstra step by step (Figures 3.27 to 3.32)

Dijkstra’s algorithm from S

Settle the closest node, relax its edges
Priority queue (unsettled, by distance)

Figures 3.27 to 3.32 in the book run Dijkstra on this graph by hand; Figure 3.26 shows the routing methods built on it.

Uniform-cost search

UCS is Dijkstra’s algorithm for one destination, adding nodes only as it finds them. Switch the stepper above to UCS to compare.

Section 3.4 of the book explains when UCS beats Dijkstra and times both (Table 3.6).

Bidirectional search

Search from both ends until the searches meet. Run both to compare the area explored.

Section 3.4 of the book shows why this halves the area (Figure 3.33) and when it is safe to stop.

Search area: one search vs. two

Part 5 · Section 3.5

Routing across a campus

Route from the King Edward VII statue to the Bahen Centre on real OpenStreetMap roads: animate one algorithm or compare them all.

Campus routing lab

Statue → Bahen Centre
explored (forward)explored (backward)routestatueBahen Centre

Road data © OpenStreetMap contributors, downloaded in 2026, so lengths differ from the book’s run.

Section 3.5 of the book builds this network with OSMnx (Figures 3.38 and 3.39) and compares each algorithm’s route in Table 3.7.

Part 6 · Code

Python in Google Colab

Open the book’s notebooks in Colab, or the companion notebook with the from-scratch code below.

Notebooks

From-scratch implementations

# Blind search on the 8-puzzle (Chapter 3, Section 3.3)
# A state is a tuple of 9 tiles read row by row; 0 is the blank. Moves slide the blank left, up, right or down.
from collections import deque
import heapq

MOVES = [("Left", -1), ("Up", -3), ("Right", 1), ("Down", 3)]      # the operator order used in the book


def expand(s):
    i = s.index(0)
    for name, d in MOVES:
        j = i + d
        if name == "Left" and i % 3 == 0 or name == "Right" and i % 3 == 2 or not 0 <= j < 9:
            continue
        t = list(s)
        t[i], t[j] = t[j], t[i]
        yield name, tuple(t)


def solvable(s):
    tiles = [t for t in s if t]
    return sum(a > b for k, a in enumerate(tiles) for b in tiles[k + 1:]) % 2 == 0


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


def graph_search(start, goal, lifo=False):
    """BFS (FIFO queue) or DFS (LIFO stack), with the goal test on generation as in algorithms 3.1 and 3.2."""
    frontier, parent, explored = deque([start]), {start: None}, set()
    in_frontier = {start}
    while frontier:
        s = frontier.pop() if lifo else frontier.popleft()
        in_frontier.discard(s)
        explored.add(s)
        for _, c in expand(s):
            if c not in explored and c not in in_frontier:
                parent[c] = s
                if c == goal:
                    return path_to(parent, c), len(explored)
                frontier.append(c)
                in_frontier.add(c)
    return None, len(explored)


def depth_limited(start, goal, limit):
    """Recursive DLS that never moves back to a state already on the current path. Returns (path, visits)."""
    visits = 0

    def rec(s, path):
        nonlocal visits
        visits += 1
        if s == goal:
            return path
        if len(path) - 1 == limit:
            return None
        for _, c in expand(s):
            if c not in path:
                r = rec(c, path + [c])
                if r:
                    return r
        return None

    return rec(start, [start]), visits


def iterative_deepening(start, goal, max_depth=30):
    total = 0
    for limit in range(max_depth + 1):
        p, v = depth_limited(start, goal, limit)
        total += v
        if p:
            return p, total, limit
    return None, total, None


def ucs(start, goal):
    """Uniform-cost search: every move costs 1, so it expands states in the same order as BFS."""
    pq, best, parent, popped, k = [(0, 0, start)], {start: 0}, {start: None}, set(), 1
    while pq:
        g, _, s = heapq.heappop(pq)
        if s in popped:
            continue
        popped.add(s)
        if s == goal:
            return path_to(parent, s), len(popped)
        for _, c in expand(s):
            if c not in popped and g + 1 < best.get(c, 1e9):
                best[c], parent[c] = g + 1, s
                heapq.heappush(pq, (g + 1, k, c)); k += 1
    return None, len(popped)


start = (1, 4, 2, 3, 7, 5, 6, 0, 8)       # Listing 3.9
goal = (0, 1, 2, 3, 4, 5, 6, 7, 8)
print("solvable:", solvable(start))
p, n = graph_search(start, goal)
print(f"BFS: {len(p) - 1} moves, {n} states explored")
p, n = graph_search(start, goal, lifo=True)
print(f"DFS: {len(p) - 1} moves, {n} states explored")
p, v = depth_limited(start, goal, 5)
print(f"DLS (limit 5): {len(p) - 1} moves, {v} states visited")
p, v, L = iterative_deepening(start, goal)
print(f"IDS: {len(p) - 1} moves, {v} states visited, depth {L}")
p, n = ucs(start, goal)
print(f"UCS: {len(p) - 1} moves, {n} states explored")
for s in p:
    print(s[:3], s[3:6], s[6:], sep="\n", end="\n\n")

To run: open the companion notebook in Colab and press Shift+Enter in each cell, or paste a tab’s code into any notebook.

Part 7 · Summary

What to remember

Graphs

Roads, social networks and puzzle states are all vertices joined by edges.

Traversal

BFS (queue) finds the fewest-edge path; DFS (stack) saves memory but can wander.

Shortest paths

Dijkstra and UCS return optimal routes on non-negative costs.

Trade-offs

Choose by time, memory and how much of the graph is known.

References: E. W. Dijkstra, “A note on two problems in connexion with graphs,” 1959. A. Khamis, Optimization Algorithms: AI techniques for design, planning, and control problems, Manning, 2024, Chapter 3. Road data © OpenStreetMap contributors (ODbL), via OSMnx.

Part 8 · Quiz

Check your understanding

Two quick questions on Chapter 3. Pick an answer to check it.

Q1. On a graph where every edge counts the same, which blind search is guaranteed to return a path with the fewest edges, and what does it pay for that guarantee?

Q2. In bidirectional Dijkstra, when is it safe to stop and report the shortest path?

For more exercises with full solutions, see Appendix C, “Exercises and solutions,” in Optimization Algorithms.