Topicsβ€ΊGraphβ€ΊA* Search Algorithm: The Ultimate Guide to Pathfinding
πŸ“–

A* Search Algorithm: The Ultimate Guide to Pathfinding

Graph
~20 min read
5 practice problems

A* Search Algorithm: The Ultimate Guide to Pathfinding

Introduction

The *A (A-Star) Search Algorithm is one of the most popular and efficient pathfinding algorithms used in computer science, particularly in artificial intelligence, game development, and robotics. It is widely considered the "gold standard" for pathfinding because it combines the optimality of Dijkstra's algorithm with the efficiency** of Greedy Best-First Search.

Unlike a standard Breadth-First Search (BFS) or Depth-First Search (DFS), A* is an informed search algorithm. This means it uses a heuristic function to estimate the cost of reaching the goal from any given node, allowing it to prioritize paths that are most likely to lead to the solution.

Core Concepts

1. The Cost Function: $f(n)$

A* evaluates every node based on a cost function $f(n)$, which is the sum of two components:

  • $g(n)$ (Actual Cost): The cost of the path from the start node to the current node $n$. This is the sum of the weights of the edges traversed.
  • $h(n)$ (Heuristic Cost): The estimated cost from the current node $n$ to the goal node. This is a guess based on domain knowledge.

The formula is:

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

2. The Heuristic Function ($h(n)$)

The heuristic function is the "brain" of A*. It must be admissible (never overestimate the true cost) and ideally consistent (monotonic).

Common Heuristics:

  1. Euclidean Distance: The straight-line distance between two points. Best for continuous spaces (like a map).

$$h(n) = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}$$

  1. Manhattan Distance: The sum of the absolute vertical and horizontal distances. Best for grid-based maps where diagonal movement is not allowed (like city blocks).

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

  1. Chebyshev Distance: The maximum of the absolute vertical and horizontal distances. Best for grid-based maps where diagonal movement is allowed (like 8-directional movement in games).

$$h(n) = \max(|x_2 - x_1|, |y_2 - y_1|)$$

3. The Open Set and Closed Set

To manage the search, A* uses two primary data structures:

  • Open Set: A collection of nodes that have been discovered but not yet explored. It is typically a Priority Queue (min-heap) sorted by $f(n)$. The node with the lowest $f(n)$ is always at the front.
  • Closed Set: A collection of nodes that have been fully explored. Once a node is in the Closed Set, it is not processed again unless a better path is found.

How A* Works: Step-by-Step

  1. Initialization:
  • Add the Start Node to the Open Set.
  • Set $g(start) = 0$ and $h(start) = heuristic(start, goal)$.
  • Set $f(start) = g(start) + h(start)$.
  1. Loop:
  • If the Open Set is empty, there is no path (return failure).
  • Remove the node with the lowest $f(n)$ from the Open Set and add it to the Closed Set. Call this node current.
  1. Goal Check:
  • If current is the Goal Node, reconstruct the path by backtracking from the goal to the start using the parent pointers and return it.
  1. Neighbor Expansion:
  • For each neighbor of current:
  • If the neighbor is in the Closed Set, skip it.
  • Calculate the tentative $g$ value: $g(current) + cost(current, neighbor)$.
  • If the neighbor is not in Open Set:
  • Set the neighbor's parent to current.
  • Calculate $f(n) = g(n) + h(n)$.
  • Add neighbor to the Open Set.
  • If the neighbor is in Open Set:
  • If the tentative $g$ is lower than the neighbor's current $g$, update the neighbor's $g$, $f$, and parent.

Implementation (TypeScript)

Here is a robust implementation of A* for a 2D grid.

interface Node {
  x: number;
  y: number;
  g: number; // Cost from start
  h: number; // Heuristic to goal
  f: number; // Total cost
  parent: Node | null;
  isWall: boolean;
}

class AStar {
  grid: Node[][];
  start: Node;
  goal: Node;
  openSet: Node[];
  closedSet: Node[];

  constructor(grid: Node[][], start: Node, goal: Node) {
    this.grid = grid;
    this.start = start;
    this.goal = goal;
    this.openSet = [];
    this.closedSet = [];
  }

  heuristic(a: Node, b: Node): number {
    // Manhattan distance
    return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
  }

  findPath(): Node[] | null {
    this.openSet.push(this.start);

    while (this.openSet.length > 0) {
      // Get node with lowest f score
      let lowestIndex = 0;
      for (let i = 0; i < this.openSet.length; i++) {
        if (this.openSet[i].f < this.openSet[lowestIndex].f) {
          lowestIndex = i;
        }
      }

      let current = this.openSet[lowestIndex];

      // Found the goal
      if (current === this.goal) {
        let path = [];
        let temp = current;
        while (temp.parent) {
          path.push(temp);
          temp = temp.parent;
        }
        path.push(this.start);
        return path.reverse();
      }

      // Move current from open to closed
      this.openSet.splice(lowestIndex, 1);
      this.closedSet.push(current);

      // Check neighbors
      const neighbors = this.getNeighbors(current);
      for (let neighbor of neighbors) {
        if (this.closedSet.includes(neighbor) || neighbor.isWall) {
          continue;
        }

        const tempG = current.g + 1; // Assuming cost of 1 per step

        let newPath = false;
        if (this.openSet.includes(neighbor)) {
          if (tempG < neighbor.g) {
            neighbor.g = tempG;
            newPath = true;
          }
        } else {
          neighbor.g = tempG;
          newPath = true;
          this.openSet.push(neighbor);
        }

        if (newPath) {
          neighbor.h = this.heuristic(neighbor, this.goal);
          neighbor.f = neighbor.g + neighbor.h;
          neighbor.parent = current;
        }
      }
    }
    return null; // No path found
  }

  getNeighbors(node: Node): Node[] {
    const neighbors: Node[] = [];
    const { x, y } = node;

    if (x < this.grid[0].length - 1) neighbors.push(this.grid[y][x + 1]);
    if (x > 0) neighbors.push(this.grid[y][x - 1]);
    if (y < this.grid.length - 1) neighbors.push(this.grid[y + 1][x]);
    if (y > 0) neighbors.push(this.grid[y - 1][x]);

    return neighbors;
  }
}

Complexity Analysis

Time Complexity: $O(b^d)$

  • $b$: Branching factor (average number of neighbors per node).
  • $d$: Depth of the solution.

In the worst case, A* explores all nodes within the search space. However, with a good heuristic, it performs significantly better than Dijkstra's algorithm.

Space Complexity: $O(b^d)$

  • This is required to store the Open Set and the Closed Set.

Comparison with Other Algorithms

| Algorithm | Optimality | Completeness | Speed | Heuristic Used |

| :--- | :--- | :--- | :--- | :--- |

| Dijkstra | Yes | Yes | Slow | None (Uniform Cost) |

| BFS | Yes (on unweighted) | Yes | Fast | None |

| Greedy Best-First | No | No | Fast | Heuristic only |

| A* | Yes | Yes | Fast | Heuristic + Cost |

Real-World Applications

  1. Video Games: Used for NPC pathfinding (finding the shortest path around obstacles), AI movement, and unit selection.
  2. GPS Navigation: Google Maps and GPS systems use A* to calculate the fastest route between two points, factoring in traffic and road types.
  3. Robotics: Used for autonomous robots to navigate environments and avoid obstacles.
  4. Network Routing: Used to find the shortest path for data packets in computer networks.

Common Mistakes to Avoid

  1. Using an Inadmissible Heuristic: If your heuristic overestimates the cost, A* might skip the optimal path. Always ensure $h(n) \leq$ actual cost to goal.
  2. Ignoring Consistency: While admissibility is enough for correctness, consistency ensures that the algorithm doesn't re-evaluate the same node multiple times, improving performance.
  3. Poor Data Structures: Using a simple array for the Open Set results in $O(N)$ search time. Always use a Priority Queue (Min-Heap) for $O(\log N)$ performance.

Summary

A Search is a powerful algorithm that balances the need to find the shortest path with the need to find it quickly*. By intelligently guiding the search using a heuristic, it avoids exploring irrelevant areas of the search space, making it the go-to choice for pathfinding problems in both theoretical computer science and practical applications.