A* Search Algorithm

A* (A-star) is an intelligent pathfinding algorithm that combines the guaranteed optimality of Dijkstra's algorithm with the efficiency of greedy best-first search. It uses heuristic guidance to find optimal paths while exploring minimal nodes.

Interactive Visualization

A* Pathfinding Visualization

50ms
Current Tool: wall - Click to place walls

Click on the grid to place start, end, and wall nodes

Grid Visualization

F-cost (top): Total cost (G + H)
G-cost (bottom left): Distance from start
H-cost (bottom right): Heuristic to end

Legend:

S
Start Node
E
End Node
Wall
Open Set
Closed Set
Current Node
Final Path
Empty

How A* Algorithm Works:

Heuristic Search: Uses distance estimate to guide search toward goal

F-cost = G-cost + H-cost: Total estimated cost through current node

G-cost: Actual distance traveled from start to current node

H-cost: Heuristic estimate from current node to goal (Manhattan distance)

Open Set: Nodes to be evaluated (frontier)

Closed Set: Nodes already evaluated

Best-First: Always expands node with lowest F-cost

Optimal: Guaranteed to find shortest path with admissible heuristic

Heuristic-Guided Search Strategy

A* employs a heuristic-guided search strategy that uses the evaluation function f(n) = g(n) + h(n) to intelligently direct the search toward the goal. This combination of actual cost and estimated remaining cost enables optimal pathfinding with maximum efficiency.

1. Evaluate

Calculate f(n) = g(n) + h(n) for each candidate node

2. Select Best

Choose node with lowest f(n) value from open set

3. Expand

Process neighbors and update costs if better path found

Key Insight

The magic of A* lies in its evaluation function f(n) = g(n) + h(n). The g(n) component ensures optimality by tracking actual costs, while h(n) provides guidance toward the goal. When h(n) is admissible (never overestimates), A* guarantees finding the optimal path.

How A* Search Works

Step-by-Step Process

1
Initialize: Add start node to open set with g(start) = 0, calculate f(start)
2
Select Node: Choose node with lowest f(n) value from open set
3
Goal Check: If current node is goal, reconstruct and return path
4
Expand: Move current to closed set, evaluate all valid neighbors
5
Update: Calculate g, h, f costs for neighbors; add to open set if better

The A* Evaluation Function

f(n) = g(n) + h(n) - Total estimated cost of path through node n

g(n): Actual cost from start node to node n

h(n): Heuristic estimate of cost from node n to goal

Admissible: h(n) ≤ actual cost from n to goal (never overestimate)

Consistent: h(n) ≤ cost(n, n') + h(n') for all neighbors n'

Example: Pathfinding with Obstacles

Setup: Start(0,0) → Goal(4,4), walls at (2,1), (2,2), (2,3)
Step 1: Expand (0,0), f-costs: (1,0)=6, (0,1)=6
Step 2: Expand (1,0), avoid blocked path, explore alternatives
Step 3: Heuristic guides search around obstacles
Step 4: Find optimal path: (0,0)→(1,0)→(1,1)→(3,1)→(4,4)
Result: Optimal path found with minimal node exploration

Heuristic Functions

Manhattan Distance

Formula: |x₁ - x₂| + |y₁ - y₂|

Best For: 4-directional movement (up, down, left, right)

Properties: Admissible, consistent, efficient to compute

Use Case: Grid-based games, robot navigation

Euclidean Distance

Formula: √[(x₁-x₂)² + (y₁-y₂)²]

Best For: 8-directional movement (including diagonals)

Properties: Admissible, consistent, more accurate

Use Case: Real-world navigation, flight paths

Heuristic Quality Impact

h(n) = 0 (Dijkstra):
Explores all directions equally
Guaranteed optimal, but slow
Maximum nodes explored
Perfect h(n):
Directly guides to goal
Explores minimal nodes
Optimal efficiency
Overestimating h(n):
May miss optimal path
Faster but not guaranteed optimal
Not admissible

Pseudocode

ALGORITHM AStar(start, goal, heuristic)
BEGIN
    // Initialize open and closed sets
    openSet = priority queue ordered by f-cost
    closedSet = empty set
    
    // Initialize start node
    start.g = 0
    start.h = heuristic(start, goal)
    start.f = start.g + start.h
    start.parent = null
    
    openSet.add(start)
    
    WHILE openSet is not empty DO
        // Get node with lowest f-cost
        current = openSet.removeMin()
        
        // Check if we reached the goal
        IF current == goal THEN
            RETURN reconstructPath(current)
        END IF
        
        // Move current from open to closed set
        closedSet.add(current)
        
        // Process all neighbors
        FOR each neighbor OF current DO
            // Skip if wall or already evaluated
            IF neighbor.isWall OR neighbor IN closedSet THEN
                CONTINUE
            END IF
            
            // Calculate tentative g-cost
            tentativeG = current.g + movementCost(current, neighbor)
            
            // Check if this path to neighbor is better
            IF tentativeG < neighbor.g THEN
                // Update neighbor
                neighbor.parent = current
                neighbor.g = tentativeG
                neighbor.h = heuristic(neighbor, goal)
                neighbor.f = neighbor.g + neighbor.h
                
                // Add to open set if not already there
                IF neighbor NOT IN openSet THEN
                    openSet.add(neighbor)
                END IF
            END IF
        END FOR
    END WHILE
    
    // No path found
    RETURN empty path
END

ALGORITHM reconstructPath(goalNode)
BEGIN
    path = empty list
    current = goalNode
    
    WHILE current is not null DO
        path.prepend(current)
        current = current.parent
    END WHILE
    
    RETURN path
END

// Heuristic functions
FUNCTION manhattanDistance(a, b)
    RETURN |a.x - b.x| + |a.y - b.y|
END

FUNCTION euclideanDistance(a, b)
    RETURN √[(a.x - b.x)² + (a.y - b.y)²]
END

Complexity Analysis

Metric Best Case Average Case Worst Case
Time Complexity O(d) O(b^d) O(b^d)
Space Complexity O(d) O(b^d) O(b^d)
Optimality Guaranteed Guaranteed Guaranteed*

*With admissible and consistent heuristic

Why A* is Efficient

  • Heuristic Guidance: Reduces search space by focusing on promising directions
  • Optimal Pruning: Admissible heuristics ensure no optimal solutions are discarded
  • Best-First Strategy: Always expands most promising nodes first
  • Early Termination: Stops immediately upon reaching goal

Factors Affecting Performance

  • Heuristic Quality: Better heuristics lead to fewer node expansions
  • Branching Factor (b): Number of neighbors per node
  • Solution Depth (d): Length of optimal path
  • Problem Structure: Obstacles and terrain complexity

Optimality Guarantees

A* guarantees finding the optimal solution when the heuristic is admissible and consistent:

Admissible: h(n) ≤ h*(n)
Never overestimates actual cost to goal
Ensures optimality
Consistent: h(n) ≤ c(n,n') + h(n')
Triangle inequality property
Ensures efficiency and optimal node expansions

Code Examples

Here are implementations of A* search in different programming languages. Each version includes both Manhattan and Euclidean distance heuristics, support for diagonal movement, and efficient priority queue management.

import heapq
import math
from typing import List, Tuple, Optional, Set

class Node:
    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y
        self.g_cost = float('inf')  # Distance from start
        self.h_cost = 0.0           # Heuristic distance to goal
        self.f_cost = float('inf')  # Total cost (g + h)
        self.parent = None
        self.is_wall = False
    
    def calculate_f_cost(self):
        self.f_cost = self.g_cost + self.h_cost
    
    def __lt__(self, other):
        if self.f_cost == other.f_cost:
            return self.h_cost < other.h_cost
        return self.f_cost < other.f_cost
    
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
    
    def __hash__(self):
        return hash((self.x, self.y))

def manhattan_distance(a: Node, b: Node) -> float:
    """Calculate Manhattan distance heuristic (admissible for 4-directional movement)"""
    return abs(a.x - b.x) + abs(a.y - b.y)

def euclidean_distance(a: Node, b: Node) -> float:
    """Calculate Euclidean distance heuristic (admissible for 8-directional movement)"""
    dx = a.x - b.x
    dy = a.y - b.y
    return math.sqrt(dx * dx + dy * dy)

def get_neighbors(node: Node, grid: List[List[Node]], allow_diagonal: bool) -> List[Node]:
    """Get valid neighboring nodes for pathfinding"""
    neighbors = []
    rows, cols = len(grid), len(grid[0])
    
    # 4-directional movement
    directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
    
    # Add diagonal directions if allowed
    if allow_diagonal:
        directions.extend([(-1, -1), (-1, 1), (1, -1), (1, 1)])
    
    for dx, dy in directions:
        new_x, new_y = node.x + dx, node.y + dy
        
        # Check bounds and walls
        if (0 <= new_x < cols and 0 <= new_y < rows):
            neighbor = grid[new_y][new_x]
            if not neighbor.is_wall:
                neighbors.append(neighbor)
    
    return neighbors

def get_movement_cost(current: Node, neighbor: Node) -> float:
    """Calculate movement cost between adjacent nodes"""
    dx = abs(current.x - neighbor.x)
    dy = abs(current.y - neighbor.y)
    
    # Diagonal movement
    if dx == 1 and dy == 1:
        return math.sqrt(2)  # ≈ 1.414
    # Straight movement
    return 1.0

def reconstruct_path(goal_node: Node) -> List[Node]:
    """Reconstruct path from goal to start using parent pointers"""
    path = []
    current = goal_node
    
    while current is not None:
        path.append(current)
        current = current.parent
    
    path.reverse()
    return path

def a_star_search(grid: List[List[Node]], start: Node, goal: Node, 
                  allow_diagonal: bool = True) -> Tuple[List[Node], int]:
    """
    A* pathfinding algorithm implementation
    
    Returns:
        Tuple containing path and number of nodes explored
    
    Time Complexity: O(b^d) where b is branching factor, d is depth
    Space Complexity: O(b^d) for storing open and closed sets
    """
    # Initialize start node
    start.g_cost = 0
    start.h_cost = euclidean_distance(start, goal) if allow_diagonal else manhattan_distance(start, goal)
    start.calculate_f_cost()
    
    # Priority queue for open set (nodes to be evaluated)
    open_set = [start]
    open_set_hash = {start}  # For O(1) membership testing
    closed_set: Set[Node] = set()  # Already evaluated nodes
    
    nodes_explored = 0
    
    while open_set:
        # Get node with lowest F-cost
        current = heapq.heappop(open_set)
        open_set_hash.remove(current)
        
        # Add to closed set
        closed_set.add(current)
        nodes_explored += 1
        
        # Check if we reached the goal
        if current == goal:
            return reconstruct_path(current), nodes_explored
        
        # Process each neighbor
        for neighbor in get_neighbors(current, grid, allow_diagonal):
            # Skip if already evaluated
            if neighbor in closed_set:
                continue
            
            # Calculate tentative G-cost
            movement_cost = get_movement_cost(current, neighbor)
            tentative_g_cost = current.g_cost + movement_cost
            
            # If this path to neighbor is better than any previous one
            if tentative_g_cost < neighbor.g_cost:
                # Update neighbor costs
                neighbor.parent = current
                neighbor.g_cost = tentative_g_cost
                neighbor.h_cost = (euclidean_distance(neighbor, goal) if allow_diagonal 
                                  else manhattan_distance(neighbor, goal))
                neighbor.calculate_f_cost()
                
                # Add to open set if not already there
                if neighbor not in open_set_hash:
                    heapq.heappush(open_set, neighbor)
                    open_set_hash.add(neighbor)
    
    # No path found
    return [], nodes_explored

def create_grid(width: int, height: int) -> List[List[Node]]:
    """Create a 2D grid of nodes"""
    grid = []
    for y in range(height):
        row = []
        for x in range(width):
            row.append(Node(x, y))
        grid.append(row)
    return grid

# Example usage
if __name__ == "__main__":
    # Create a 10x10 grid
    grid = create_grid(10, 10)
    
    # Set start and goal
    start = grid[0][0]
    goal = grid[9][9]
    
    # Add some walls
    walls = [(3, 3), (3, 4), (3, 5), (4, 3), (5, 3)]
    for x, y in walls:
        grid[y][x].is_wall = True
    
    # Run A* search
    path, explored = a_star_search(grid, start, goal, allow_diagonal=True)
    
    if path:
        print(f"Path found! Length: {len(path)} nodes")
        print(f"Nodes explored: {explored}")
        print("Path coordinates:")
        for node in path:
            print(f"  ({node.x}, {node.y})")
    else:
        print("No path found!")
        print(f"Nodes explored: {explored}")

Practical Applications

Excellent For

  • Video Game AI: NPC pathfinding with optimal routes and natural movement
  • Robotics Navigation: Mobile robot path planning with obstacle avoidance
  • GPS Route Planning: Finding optimal routes considering traffic and distance
  • Puzzle Solving: 15-puzzle, sliding puzzles, maze solving
  • AI Planning: Task scheduling and resource allocation
  • Network Routing: Optimal packet routing with QoS constraints

Consider Alternatives When

  • Very Large Search Spaces: Memory requirements become prohibitive
  • No Good Heuristic: When h(n) provides little guidance
  • Any Path Acceptable: When optimality is not required (use greedy search)
  • Dynamic Environments: Frequently changing obstacles or costs
  • Real-Time Constraints: When strict time limits require approximate solutions

Real-World Applications

Entertainment & Games: Strategy games (StarCraft, Age of Empires), RPG character movement, procedural level generation, AI opponent behavior, racing game optimal racing lines, puzzle game solvers
Industrial Applications: Autonomous vehicle navigation, drone flight path optimization, robotic arm motion planning, warehouse automation, 3D printing toolpath generation, manufacturing workflow optimization