1+ """
2+ Algorithm: A* (A-star) Search
3+ Description: Finds the shortest path between a start node and a goal node using
4+ g(n) (actual cost so far) + h(n) (heuristic).
5+ Time Complexity: O(E) in worst case
6+ Space Complexity: O(V)
7+ Author: Kashyap
8+ Date: 2025-10-04
9+ """
10+
11+ import heapq
12+
13+ class Node :
14+ def __init__ (self , position , parent = None , g = 0 , h = 0 ):
15+ self .position = position # (row, col)
16+ self .parent = parent # previous node in path
17+ self .g = g # cost from start
18+ self .h = h # heuristic (to goal)
19+ self .f = g + h # total cost
20+
21+ def __lt__ (self , other ):
22+ return self .f < other .f # needed for heapq priority
23+
24+ def heuristic (a , b ):
25+ """Manhattan distance heuristic"""
26+ return abs (a [0 ] - b [0 ]) + abs (a [1 ] - b [1 ])
27+
28+ def a_star (grid , start , goal ):
29+ """
30+ Perform A* search on a grid.
31+
32+ Args:
33+ grid (list[list[int]]): 0 = free cell, 1 = obstacle
34+ start (tuple): (row, col) start position
35+ goal (tuple): (row, col) goal position
36+
37+ Returns:
38+ path (list[tuple]): shortest path from start to goal
39+ """
40+ open_set = []
41+ heapq .heappush (open_set , Node (start , None , 0 , heuristic (start , goal )))
42+ visited = set ()
43+
44+ while open_set :
45+ current = heapq .heappop (open_set )
46+
47+ if current .position == goal :
48+ # Reconstruct path
49+ path = []
50+ while current :
51+ path .append (current .position )
52+ current = current .parent
53+ return path [::- 1 ] # reverse path
54+
55+ visited .add (current .position )
56+
57+ # Explore neighbors (up, down, left, right)
58+ directions = [(0 ,1 ), (0 ,- 1 ), (1 ,0 ), (- 1 ,0 )]
59+ for d in directions :
60+ neighbor = (current .position [0 ] + d [0 ], current .position [1 ] + d [1 ])
61+
62+ # Skip if out of bounds or obstacle
63+ if (0 <= neighbor [0 ] < len (grid ) and
64+ 0 <= neighbor [1 ] < len (grid [0 ]) and
65+ grid [neighbor [0 ]][neighbor [1 ]] == 0 and
66+ neighbor not in visited ):
67+
68+ g_cost = current .g + 1
69+ h_cost = heuristic (neighbor , goal )
70+ heapq .heappush (open_set , Node (neighbor , current , g_cost , h_cost ))
71+
72+ return None # no path found
73+
74+
75+ # main func()
76+ if __name__ == "__main__" :
77+ # Test Case 1: Normal case
78+ grid1 = [
79+ [0 , 1 , 0 , 0 , 0 ],
80+ [0 , 1 , 0 , 1 , 0 ],
81+ [0 , 0 , 0 , 1 , 0 ],
82+ [0 , 1 , 0 , 0 , 0 ],
83+ [0 , 0 , 0 , 1 , 0 ]
84+ ]
85+ start1 = (0 , 0 )
86+ goal1 = (4 , 4 )
87+ path1 = a_star (grid1 , start1 , goal1 )
88+ print ("Test Case 1 Path:" , path1 ) # Expected: a valid path like [(0,0),(1,0),(2,0),(2,1),...(4,4)]
89+
90+ # Test Case 2: Blocked path
91+ grid2 = [
92+ [0 , 1 , 1 ],
93+ [1 , 1 , 1 ],
94+ [0 , 0 , 0 ]
95+ ]
96+ start2 = (0 , 0 )
97+ goal2 = (2 , 2 )
98+ path2 = a_star (grid2 , start2 , goal2 )
99+ print ("Test Case 2 Path:" , path2 ) # Expected: None (no path exists)
100+
101+ # Test Case 3: Start = Goal
102+ grid3 = [
103+ [0 , 0 ],
104+ [0 , 0 ]
105+ ]
106+ start3 = (0 , 0 )
107+ goal3 = (0 , 0 )
108+ path3 = a_star (grid3 , start3 , goal3 )
109+ print ("Test Case 3 Path:" , path3 ) # Expected: [(0, 0)] (trivial path)
110+
111+ # Test Case 4: Larger grid with obstacles
112+ grid4 = [
113+ [0 , 0 , 0 , 0 , 0 , 0 ],
114+ [1 , 1 , 1 , 1 , 1 , 0 ],
115+ [0 , 0 , 0 , 0 , 0 , 0 ],
116+ [0 , 1 , 1 , 1 , 1 , 1 ],
117+ [0 , 0 , 0 , 0 , 0 , 0 ]
118+ ]
119+ start4 = (0 , 0 )
120+ goal4 = (4 , 5 )
121+ path4 = a_star (grid4 , start4 , goal4 )
122+ print ("Test Case 4 Path:" , path4 ) # Expected: a valid path from top-left to bottom-right
0 commit comments