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
Click on the grid to place start, end, and wall nodes
Grid Visualization
Legend:
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
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
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
Explores all directions equally
Guaranteed optimal, but slow
Maximum nodes explored
Directly guides to goal
Explores minimal nodes
Optimal efficiency
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:
Never overestimates actual cost to goal
Ensures optimality
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