Skip to content
academia.sh

Lesson 17 / 25

A* Search

Target-directed search with a heuristic function, admissibility and consistency conditions, the relation to Dijkstra, and the gain in expanded vertex count.

Published Updated

In this lesson

  • Explains the f = g + h evaluation and the search order
  • Defines an admissible and a consistent heuristic and shows its effect on optimality
  • Measures the effect of heuristic quality on the number of expanded vertices
Contents

The previous two algorithms computed distance from the source to every vertex. This is wasteful if a specific target is the goal: vertices in the opposite direction from the target are processed with the same care.

A* reduces this waste by folding an estimate of where the target lies into the search.

The Evaluation Function

Two quantities are kept for each vertex:

  • g(v)g(v) — the best known cost from the source to v (the distance in Dijkstra’s algorithm).
  • h(v)h(v) — the estimated remaining cost from v to the target; it comes from knowledge of the problem.

The search proceeds by their sum:

f(v)=g(v)+h(v)f(v) = g(v) + h(v)

f(v)f(v) is the estimated total cost of a solution passing through v. The priority queue orders not by distance but by this value; everything else is the same as Dijkstra’s algorithm.

import heapq


def a_star(graph: dict[str, list[tuple[str, int]]],
           start: str, target: str, h: dict[str, int]) -> float | None:
    g: dict[str, float] = {start: 0}
    open_set: list[tuple[float, float, str]] = [(h[start], 0, start)]
    closed: set[str] = set()

    while open_set:
        _, cost, vertex = heapq.heappop(open_set)
        if vertex in closed:
            continue
        closed.add(vertex)
        if vertex == target:
            return cost
        for neighbor, weight in graph[vertex]:
            new_cost = cost + weight
            if new_cost < g.get(neighbor, float("inf")):
                g[neighbor] = new_cost
                heapq.heappush(open_set, (new_cost + h[neighbor], new_cost, neighbor))
    return None

If h(v)=0h(v) = 0 for every vertex, f=gf = g and the algorithm becomes exactly Dijkstra’s algorithm. A* is, in this sense, a generalization of Dijkstra: the heuristic is the extra information added to the search.

Admissibility

The heuristic cannot be arbitrary. Being admissible means it never exceeds the true remaining cost at any vertex:

h(v)≤h∗(v)h(v) \leq h^*(v)

Here h∗(v)h^*(v) is the true remaining cost. With an admissible heuristic, A* guarantees that the first solution it finds is optimal.

Justification: when the target reaches the front of the queue, f(target)=g(target)f(\text{target}) = g(\text{target}). If a better solution existed, a vertex on that solution’s path would be in the queue, and — since estimates never overshoot — its ff value would be smaller than the true total cost, so it would be popped before the target.

If the condition is violated, optimality is lost.

graph = {
    "A": [("B", 1), ("C", 5)],
    "B": [("H", 10)],
    "C": [("H", 1)],
    "H": [],
}

overestimating = {"A": 0, "B": 1, "C": 10, "H": 0}     # C's estimate is far above the true value (1)
admissible = {"A": 6, "B": 10, "C": 1, "H": 0}

print(a_star(graph, "A", "H", overestimating))         # 11  — not optimal
print(a_star(graph, "A", "H", admissible))              # 6
print(a_star(graph, "A", "H", {v: 0 for v in graph}))   # 6   — Dijkstra

The overestimate causes the true shortest path through C (A→C→H, 6) to be pruned away; the algorithm returns the A→B→H path (11) instead.

Consistency

A stronger condition is consistency: for every edge (v, w),

h(v)≤weight(v,w)+h(w)h(v) \leq \text{weight}(v, w) + h(w)

This is the heuristic’s counterpart of the triangle inequality. A consistent heuristic is admissible and additionally guarantees the following: when a vertex is closed, its gg value is final, so no vertex is ever expanded twice.

With a heuristic that is admissible but not consistent, the gg value of a closed vertex may not be final. Optimality is preserved only if a closed vertex is reopened when it is reached by a cheaper path; the implementation above skips closed vertices unconditionally, so with such a heuristic it can return a path that is not optimal. Most heuristics used in practice — straight-line distance on a plane, Manhattan distance on a grid — are consistent, so the situation does not arise.

The Measured Gain

The gain shows up in the number of expanded vertices. In the grid below, every step costs one unit; neighbors lie in four directions.

def grid_search(grid: list[str], start: tuple[int, int],
                target: tuple[int, int], heuristic) -> tuple[int, int]:
    """(path cost, number of expanded vertices)"""
    g = {start: 0}
    open_set = [(heuristic(start, target), 0, start)]
    closed: set[tuple[int, int]] = set()
    count = 0

    while open_set:
        _, cost, vertex = heapq.heappop(open_set)
        if vertex in closed:
            continue
        closed.add(vertex)
        count += 1
        if vertex == target:
            return cost, count
        row, col = vertex
        for dr, dc in ((-1, 0), (0, -1), (0, 1), (1, 0)):
            r, c = row + dr, col + dc
            if 0 <= r < len(grid) and 0 <= c < len(grid[0]) and grid[r][c] != "#":
                new_cost = cost + 1
                if new_cost < g.get((r, c), float("inf")):
                    g[(r, c)] = new_cost
                    heapq.heappush(open_set, (new_cost + heuristic((r, c), target), new_cost, (r, c)))
    return -1, count


zero = lambda a, b: 0
manhattan = lambda a, b: abs(a[0] - b[0]) + abs(a[1] - b[1])

open_grid = ["." * 15 for _ in range(15)]
walled = ["".join("#" if (2 <= r <= 12 and c == 7) else "." for c in range(15))
          for r in range(15)]

print(grid_search(open_grid, (7, 1), (7, 13), zero))       # (12, 147)
print(grid_search(open_grid, (7, 1), (7, 13), manhattan))  # (12, 13)
print(grid_search(walled, (7, 1), (7, 13), zero))          # (24, 212)
print(grid_search(walled, (7, 1), (7, 13), manhattan))     # (24, 169)

On the open grid, A* follows the strip leading to the target and expands 13 vertices instead of 147. The path cost is identical — the gain is not in correctness, but in work spent.

On the walled grid, the gain erodes: Manhattan distance does not see the wall, so the estimate stays far below the truth and the heuristic does not narrow the search direction enough. A heuristic’s quality is measured by how close it comes to the truth; every point between h=0h = 0 and h=h∗h = h^* lies somewhere between Dijkstra and walking straight to the target.

Trade-offs

Memory. A* stores every vertex in the open set; in large search spaces, memory is as limiting as time. Variants that iterate by increasing depth (O(b d)O(b\,d) memory) solve this problem at a cost.

Deliberate overestimation. Scaling the heuristic by a factor narrows the search but breaks optimality; it is used knowing the solution found may be worse by that factor. This is a reasonable trade-off when a fast answer is worth more than an optimal one.

The heuristic’s own cost. hh is computed for every vertex; an expensive heuristic can take away more than the expansions it saves.

These trade-offs are the artificial-intelligence side of search, and together with game trees they are the subject of the AI Engineering curriculum; they are not treated here.

Summary

  • A* expands vertices by their f(v)=g(v)+h(v)f(v) = g(v) + h(v) value; with h=0h = 0 it reduces to Dijkstra’s algorithm.
  • An admissible heuristic never exceeds the true remaining cost and guarantees the solution found is optimal.
  • A consistent heuristic satisfies the triangle inequality; no vertex is ever expanded twice.
  • The gain shows up in the number of expanded vertices and is proportional to how close the heuristic is to the truth.
  • Obstacles push the estimate further from the truth and reduce the gain.
  • Memory use, deliberate overestimation, and the heuristic’s own computational cost are the main trade-offs.

Next Step

The question asked so far has been “what is the cheapest path between two vertices.” The next lesson changes the question: what is the cheapest set of edges connecting all vertices to each other? The minimum spanning tree problem will establish the correctness of a greedy choice on different grounds — the cut property.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close