A* Pathfinding Algorithm

algorithms
search
graph theory
A* finds the shortest path between two points by balancing actual travel cost and a heuristic estimate of remaining distance.
Author

Andreas Bogossian

Published

March 19, 2026

How does Google Maps find the best route in under a second, across millions of roads? Stripping this question into its basic elements, the question becomes, how to find the shortest path in a graph from the start position to the end position. If one was to approach this problem by trying every single path, the number of paths grow as \mathcal{O}(b^d), where b is the branching factor (average number of branches from each node) and d is the depth. The brute force method scales extremely poorly so another solution must be used.

Dijkstra’s algorithm improves on the brute force method slightly. Dijkstra always expands the cheapest node so far. The problem with Dijkstra is that it expands the search in every direction including roads that lead away from the destination. Dijkstra can be beaten with algorithms with sense of direction.

The idea behind the A* algorithm

A* assigns every node a single score:

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

where

  • g(n): the exact cost from start to the current node n. This is known.
  • h(n): the estimated cost from the current node n to the goal. This is a heuristic (educated guess).

After A* computes the score of each frontier node, the algorithm will always expand the node with the lowest f(n). Minimizing both the cost of getting to the current node g(n) and the estimate of arriving at the goal h(n) makes the algorithm seek the shortest path from start point to the end point.

For a more thorough study of A*, please refer to [1].

Implementation

Heuristic

A common heuristic is the Manhattan distance:

h(n) = |x_1 - x_2| + |y_1 - y_2|

The geometric interpretation of this is amount of grid cells that need to be navigated to go from one point to another without moving diagonally.

def heuristic(a: tuple[int, int], b: tuple[int, int]) -> int:
    return abs(a[0] - b[0]) + abs(a[1] - b[1])

Main loop

The open set is a min-heap keyed on f(n). At each iteration the node with the lowest f is popped. Then check if the goal has been reached. Then the third step is to expand the neighbours:

import heapq

def astar(
    grid: list[list[int]],
    start: tuple[int, int],
    goal: tuple[int, int]
) -> dict[tuple[int, int], tuple[int, int]]:
    rows, cols = len(grid), len(grid[0])

    open_set: list[tuple[int, tuple[int, int]]] = []
    heapq.heappush(open_set, (0, start))

    came_from: dict[tuple[int, int], tuple[int, int]] = {}
    g_score: dict[tuple[int, int], int] = {start: 0}

    while open_set:
        _, current = heapq.heappop(open_set)

        if current == goal:
            break

        r, c = current
        for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
            neighbour: tuple[int, int] = (r + dr, c + dc)
            nr, nc = neighbour

            if not (0 <= nr < rows and 0 <= nc < cols):
                continue
            if grid[nr][nc] == 1:
                continue

            tentative_g = g_score[current] + 1

            if tentative_g < g_score.get(neighbour, float('inf')):
                came_from[neighbour] = current
                g_score[neighbour] = tentative_g
                f = tentative_g + heuristic(neighbour, goal)
                heapq.heappush(open_set, (f, neighbour))

    return came_from

Path reconstruction

def reconstruct_path(
    came_from: dict[tuple[int, int], tuple[int, int]],
    start: tuple[int, int],
    goal: tuple[int, int]
) -> list[tuple[int, int]]:
    path: list[tuple[int, int]] = []
    node = goal
    while node != start:
        path.append(node)
        node = came_from[node]
    path.append(start)
    path.reverse()
    return path

Running the algorithm on a 2D grid

grid = [
    [0, 0, 0, 1, 0],
    [1, 1, 0, 1, 0],
    [0, 0, 0, 0, 0],
    [0, 1, 1, 1, 0],
    [0, 0, 0, 0, 0],
]
start, goal = (0, 0), (4, 4)

came_from = astar(grid, start, goal)
path = reconstruct_path(came_from, start, goal)
print(path)
[(0, 0), (0, 1), (0, 2), (1, 2), (2, 2), (2, 3), (2, 4), (3, 4), (4, 4)]

Weighted A*

A useful, greedier A* variant is weighted A*. The variant inflates the heuristic by a weight w > 1:

f(n) = g(n) + w \cdot h(n)

The benefit of using the weight is that fewer nodes are expanded because the search prioritizes nodes that are close to the end. The trade-off is that the path found is at most w times longer than optimal.

Demo

Start with Dijkstra and then swap to A* with the Manhattan heuristic. Also, test the weight slider to make the algorithm faster with the expense of accuracy.

  • Blue-green cells — open set: discovered, not yet expanded.
  • Orange cell — node currently being expanded.
  • Dark green cells — closed set: fully expanded.
  • Yellow path — the solution once the algorithm reaches the end node.
  • Drag S or E to reposition start and goal. Paint walls by clicking and dragging on empty cells.

Discussion

A* shines in applications where a cost function and an admissible heuristic (a heuristic that never overestimates the distance to the end) can be defined. Examples of these types of application are GPS routing, NPC path finding in games and network routing.

There is one problem with A*: it doesn’t factor walls into the heuristic when using Manhattan distance. For solving this problem, algorithms like Jump Point Search (JPS) are used.

References

[1]
S. Russell and P. Norvig, Artificial intelligence: A modern approach, 4th ed. Pearson Education Limited, 2022.

Reuse

Citation

BibTeX citation:
@online{bogossian2026,
  author = {Bogossian, Andreas},
  title = {A* {Pathfinding} {Algorithm}},
  date = {2026-03-19},
  url = {https://andreasbogossian.com/posts/a-star/},
  langid = {en}
}
For attribution, please cite this work as:
A. Bogossian, “A* Pathfinding Algorithm.” [Online]. Available: https://andreasbogossian.com/posts/a-star/