The A* search algorithm is a pathfinding algorithm used to find the shortest path between a starting point and a destination. It combines the actual cost of reaching a cell with a heuristic estimate of the remaining distance.
Examples: Consider a grid where 1 represents an unblocked cell and 0 represents a blocked cell.
Input:
Start: (8, 0)
Destination: (0, 0)
Output:
(8, 0) → (7, 0) → (6, 0) → (5, 0) → (4, 1) → (3, 2) → (2, 1) → (1, 0) → (0, 0)Explanation:
A* searches through the available cells and selects the path with the lowest estimated total cost from the source to the destination.
How A* Search Algorithm Works?
A* calculates three values for each cell:
- g: Cost of reaching the current cell from the source.
- h: Estimated cost from the current cell to the destination.
- f: Total estimated cost, calculated as f = g + h.
The algorithm selects the cell with the lowest f value and continues searching until the destination is reached.
Approach
The algorithm uses a priority queue to process the cell with the lowest f value first.
- Create a priority queue and add the source cell.
- Calculate the g, h, and f values for each reachable cell.
- Select the cell with the lowest f value.
- Check its neighboring cells.
- Update a neighbor when a lower-cost path is found.
- Store the parent of each cell to reconstruct the final path.
- Stop when the destination is reached.
Step-by-Step Implementation
Step 1: Import Required Modules and Define the Cell Class
The Cell class stores the f, g, h values and parent position for each grid cell.
import heapq
class Cell:
def __init__(self):
self.parent_i = 0
self.parent_j = 0
self.f = float('inf')
self.g = float('inf')
self.h = 0
Explanation:
- heapq is used to create the priority queue.
- Cell stores the f, g, and h values for each cell.
- parent_i and parent_j store the position of the previous cell.
Step 2: Define Grid Helper Functions
These functions check valid cells, blocked cells, destination cells, and calculate the heuristic value.
ROW = 9
COL = 10
def is_valid(row, col):
return 0 <= row < ROW and 0 <= col < COL
def is_unblocked(grid, row, col):
return grid[row][col] == 1
def is_destination(row, col, dest):
return row == dest[0] and col == dest[1]
def calculate_h_value(row, col, dest):
return ((row - dest[0]) ** 2 + (col - dest[1]) ** 2) ** 0.5
Explanation:
- is_valid() checks whether a cell is inside the grid.
- is_unblocked() checks whether the cell can be visited.
- is_destination() checks whether the current cell is the destination.
- calculate_h_value() calculates the estimated distance to the destination.
Step 3: Reconstruct the Path
The trace_path() function follows the parent cells from the destination to the source and reverses the path.
def trace_path(cell_details, dest):
path = []
row, col = dest
while not (
cell_details[row][col].parent_i == row
and cell_details[row][col].parent_j == col
):
path.append((row, col))
row = cell_details[row][col].parent_i
col = cell_details[row][col].parent_j
path.append((row, col))
path.reverse()
print("The Path is:")
for cell in path:
print("->", cell, end=" ")
print()
Explanation:
- The function starts from the destination cell.
- It follows the parent cells until it reaches the source.
- reverse() changes the path from source-to-destination order.
Step 4: Initialize the A* Search
The a_star_search() function validates the source and destination and initializes the visited cells and priority queue.
def a_star_search(grid, src, dest):
if not is_valid(src[0], src[1]) or not is_valid(dest[0], dest[1]):
print("Source or destination is invalid")
return
if not is_unblocked(grid, src[0], src[1]) or not is_unblocked(grid, dest[0], dest[1]):
print("Source or destination is blocked")
return
if is_destination(src[0], src[1], dest):
print("We are already at the destination")
return
closed_list = [[False for _ in range(COL)] for _ in range(ROW)]
cell_details = [[Cell() for _ in range(COL)] for _ in range(ROW)]
i, j = src
cell_details[i][j].f = 0
cell_details[i][j].g = 0
cell_details[i][j].h = 0
cell_details[i][j].parent_i = i
cell_details[i][j].parent_j = j
open_list = []
heapq.heappush(open_list, (0.0, i, j))
Explanation:
- The function first checks whether the source and destination are valid and unblocked.
- closed_list keeps track of visited cells.
- cell_details stores information about every cell.
- The source cell is added to the priority queue with an initial cost of 0.
Step 5: Explore Neighboring Cells
The algorithm removes the cell with the lowest f value and checks its valid neighboring cells.
directions = [
(0, 1), (0, -1), (1, 0), (-1, 0),
(1, 1), (1, -1), (-1, 1), (-1, -1)
]
while open_list:
_, i, j = heapq.heappop(open_list)
if closed_list[i][j]:
continue
closed_list[i][j] = True
for di, dj in directions:
new_i = i + di
new_j = j + dj
if not is_valid(new_i, new_j):
continue
if not is_unblocked(grid, new_i, new_j):
continue
if closed_list[new_i][new_j]:
continue
Explanation:
- The algorithm removes the cell with the lowest f value from the priority queue.
- It marks the selected cell as visited.
- The eight possible neighboring directions are checked.
- Invalid, blocked, or already visited cells are skipped.
Step 6: Calculate Costs and Update the Queue
For each valid neighbor, the algorithm calculates g, h, and f values and adds better paths to the priority queue.
if is_destination(new_i, new_j, dest):
cell_details[new_i][new_j].parent_i = i
cell_details[new_i][new_j].parent_j = j
print("The destination cell is found")
trace_path(cell_details, dest)
return
g_new = cell_details[i][j].g + 1.0
h_new = calculate_h_value(new_i, new_j, dest)
f_new = g_new + h_new
if cell_details[new_i][new_j].f > f_new:
cell_details[new_i][new_j].f = f_new
cell_details[new_i][new_j].g = g_new
cell_details[new_i][new_j].h = h_new
cell_details[new_i][new_j].parent_i = i
cell_details[new_i][new_j].parent_j = j
heapq.heappush(
open_list,
(f_new, new_i, new_j)
)
print("Failed to find the destination cell")
Explanation:
- If a neighboring cell is the destination, its parent is stored and the path is reconstructed.
- g_new represents the cost from the source to the current cell.
- h_new represents the estimated cost to the destination.
- f_new = g_new + h_new gives the total estimated cost.
- If a better path is found, the cell information and priority queue are updated.
Step 7: Run the A* Search
The grid, source, and destination are defined and passed to the A* search function.
def main():
grid = [
[1, 0, 1, 1, 1, 1, 0, 1, 1, 1],
[1, 1, 1, 0, 1, 1, 1, 0, 1, 1],
[1, 1, 1, 0, 1, 1, 0, 1, 0, 1],
[0, 0, 1, 0, 1, 0, 0, 0, 0, 1],
[1, 1, 1, 0, 1, 1, 1, 0, 1, 0],
[1, 0, 1, 1, 1, 1, 0, 1, 0, 0],
[1, 0, 0, 0, 0, 1, 0, 0, 0, 1],
[1, 0, 1, 1, 1, 1, 0, 1, 1, 1],
[1, 1, 1, 0, 0, 0, 1, 0, 0, 1]
]
src = [8, 0]
dest = [0, 0]
a_star_search(grid, src, dest)
if __name__ == "__main__":
main()
Output:
The destination cell is found
The Path is
-> (8, 0) -> (7, 0) -> (6, 0) -> (5, 0) -> (4, 1) -> (3, 2) -> (2, 1) -> (1, 0) -> (0, 0)
Explanation:
- The grid uses 1 for unblocked cells and 0 for blocked cells.
- src defines the starting cell and dest defines the destination cell.
- a_star_search() runs the A* algorithm using the given grid and positions.