Skip to content

Commit b511072

Browse files
committed
Adding A* Search Graph
1 parent a9c214a commit b511072

File tree

2 files changed

+227
-0
lines changed

2 files changed

+227
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ This is my study repository.
2424
* Graph Depth-First Search (DFS).
2525
* Graph with Breadth-First Search (BFS).
2626
* Greedy Search Graph.
27+
* A* Search Graph.
2728

2829

2930
## Link

src/a_search_graph.py

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
#!/usr/bin/env python3
2+
3+
##
4+
# A* Search Graph
5+
#
6+
# Problem: From Arad to Bucharest
7+
# Source: Artificial Intelligence - Stuart Russell and Peter Norvig
8+
##
9+
10+
import numpy as np
11+
12+
class Vertex:
13+
"""
14+
A class representing a vertex (node) in the graph.
15+
16+
Attributes:
17+
label (str): The label of the vertex.
18+
visited (bool): Whether the vertex has been visited in the search.
19+
distance_to_goal (int): Estimated distance from this vertex to the goal (heuristic).
20+
adjacencies (list): List of adjacent vertices and their corresponding costs.
21+
g_cost (int): The actual cost from the start node to this vertex.
22+
parent (Vertex): The parent vertex for path reconstruction.
23+
"""
24+
25+
def __init__(self, label, distance_to_goal):
26+
self.label = label
27+
self.visited = False
28+
self.distance_to_goal = distance_to_goal # Heuristic (h(n))
29+
self.adjacencies = []
30+
self.g_cost = float('inf') # Actual cost from the start node (g(n))
31+
self.parent = None # Parent vertex for path reconstruction
32+
33+
def add_adjacent(self, adjacent):
34+
"""
35+
Adds an adjacent vertex to the current vertex.
36+
37+
Args:
38+
adjacent (Adjacent): The adjacent vertex and its cost.
39+
"""
40+
self.adjacencies.append(adjacent)
41+
42+
def show_adjacencies(self):
43+
"""
44+
Displays all adjacent vertices with their associated costs.
45+
"""
46+
for adj in self.adjacencies:
47+
print(f'{adj.vertex.label} - Cost: {adj.cost}')
48+
49+
class Adjacent:
50+
"""
51+
A class representing an edge between two vertices.
52+
53+
Attributes:
54+
vertex (Vertex): The adjacent vertex.
55+
cost (int): The cost to move from the current vertex to the adjacent vertex.
56+
total_cost (int): The sum of the edge cost and the heuristic for the adjacent vertex (A*).
57+
"""
58+
59+
def __init__(self, vertex, cost):
60+
self.vertex = vertex
61+
self.cost = cost
62+
self.total_cost = vertex.distance_to_goal + self.cost # A* heuristic + cost
63+
64+
class Graph:
65+
"""
66+
A class representing the entire graph with vertices and their adjacencies.
67+
"""
68+
69+
# Define vertices
70+
arad = Vertex('Arad', 366)
71+
zerind = Vertex('Zerind', 374)
72+
oradea = Vertex('Oradea', 380)
73+
sibiu = Vertex('Sibiu', 253)
74+
timisoara = Vertex('Timisoara', 329)
75+
lugoj = Vertex('Lugoj', 244)
76+
mehadia = Vertex('Mehadia', 241)
77+
dobreta = Vertex('Dobreta', 242)
78+
craiova = Vertex('Craiova', 160)
79+
rimnicu = Vertex('Rimnicu', 193)
80+
fagaras = Vertex('Fagaras', 178)
81+
pitesti = Vertex('Pitesti', 98)
82+
bucharest = Vertex('Bucharest', 0)
83+
giurgiu = Vertex('Giurgiu', 77)
84+
85+
# Add adjacencies
86+
arad.add_adjacent(Adjacent(zerind, 75))
87+
arad.add_adjacent(Adjacent(sibiu, 140))
88+
arad.add_adjacent(Adjacent(timisoara, 118))
89+
90+
zerind.add_adjacent(Adjacent(arad, 75))
91+
zerind.add_adjacent(Adjacent(oradea, 71))
92+
93+
oradea.add_adjacent(Adjacent(zerind, 71))
94+
oradea.add_adjacent(Adjacent(sibiu, 151))
95+
96+
sibiu.add_adjacent(Adjacent(oradea, 151))
97+
sibiu.add_adjacent(Adjacent(arad, 140))
98+
sibiu.add_adjacent(Adjacent(fagaras, 99))
99+
sibiu.add_adjacent(Adjacent(rimnicu, 80))
100+
101+
timisoara.add_adjacent(Adjacent(arad, 118))
102+
timisoara.add_adjacent(Adjacent(lugoj, 111))
103+
104+
lugoj.add_adjacent(Adjacent(timisoara, 111))
105+
lugoj.add_adjacent(Adjacent(mehadia, 70))
106+
107+
mehadia.add_adjacent(Adjacent(lugoj, 70))
108+
mehadia.add_adjacent(Adjacent(dobreta, 75))
109+
110+
dobreta.add_adjacent(Adjacent(mehadia, 75))
111+
dobreta.add_adjacent(Adjacent(craiova, 120))
112+
113+
craiova.add_adjacent(Adjacent(dobreta, 120))
114+
craiova.add_adjacent(Adjacent(pitesti, 138))
115+
craiova.add_adjacent(Adjacent(rimnicu, 146))
116+
117+
rimnicu.add_adjacent(Adjacent(craiova, 146))
118+
rimnicu.add_adjacent(Adjacent(sibiu, 80))
119+
rimnicu.add_adjacent(Adjacent(pitesti, 97))
120+
121+
fagaras.add_adjacent(Adjacent(sibiu, 99))
122+
fagaras.add_adjacent(Adjacent(bucharest, 211))
123+
124+
pitesti.add_adjacent(Adjacent(rimnicu, 97))
125+
pitesti.add_adjacent(Adjacent(craiova, 138))
126+
pitesti.add_adjacent(Adjacent(bucharest, 101))
127+
128+
bucharest.add_adjacent(Adjacent(fagaras, 211))
129+
bucharest.add_adjacent(Adjacent(pitesti, 101))
130+
bucharest.add_adjacent(Adjacent(giurgiu, 90))
131+
132+
# Create the graph object
133+
graph = Graph()
134+
135+
class OrderedList:
136+
"""
137+
A class representing a sorted list of adjacent vertices for A* search.
138+
139+
Attributes:
140+
capacity (int): Maximum capacity of the list.
141+
values (list): A list of adjacent vertices sorted by total cost.
142+
"""
143+
144+
def __init__(self, capacity):
145+
self.capacity = capacity
146+
self.last_position = -1
147+
self.values = np.empty(self.capacity, dtype=object)
148+
149+
def insert(self, adjacent):
150+
"""
151+
Inserts an adjacent vertex into the sorted list based on its total A* cost.
152+
153+
Args:
154+
adjacent (Adjacent): The adjacent vertex to insert.
155+
"""
156+
if self.last_position == self.capacity - 1:
157+
print('Maximum capacity reached')
158+
return
159+
160+
position = 0
161+
for i in range(self.last_position + 1):
162+
position = i
163+
if self.values[i].total_cost > adjacent.total_cost:
164+
break
165+
if i == self.last_position:
166+
position = i + 1
167+
168+
x = self.last_position
169+
while x >= position:
170+
self.values[x + 1] = self.values[x]
171+
x -= 1
172+
self.values[position] = adjacent
173+
self.last_position += 1
174+
175+
def print_list(self):
176+
"""
177+
Prints the list of adjacent vertices with their associated costs.
178+
"""
179+
if self.last_position == -1:
180+
print('The list is empty')
181+
else:
182+
for i in range(self.last_position + 1):
183+
print(f'{i} - {self.values[i].vertex.label} - Cost: {self.values[i].cost} - '
184+
f'Heuristic: {self.values[i].vertex.distance_to_goal} - Total: {self.values[i].total_cost}')
185+
186+
class AStarSearch:
187+
"""
188+
A* Search Algorithm to find the path from the start node to the goal node.
189+
190+
Attributes:
191+
goal (Vertex): The goal vertex.
192+
found (bool): Whether the goal has been found.
193+
"""
194+
195+
def __init__(self, goal):
196+
self.goal = goal
197+
self.found = False
198+
199+
def search(self, current):
200+
"""
201+
Perform the A* search to find the goal.
202+
203+
Args:
204+
current (Vertex): The current vertex in the search.
205+
"""
206+
print('----------')
207+
print(f'Current: {current.label}')
208+
current.visited = True
209+
210+
if current == self.goal:
211+
self.found = True
212+
print('Goal found!')
213+
else:
214+
ordered_list = OrderedList(len(current.adjacencies))
215+
for adjacent in current.adjacencies:
216+
if not adjacent.vertex.visited:
217+
adjacent.vertex.visited = True
218+
ordered_list.insert(adjacent)
219+
ordered_list.print_list()
220+
221+
if ordered_list.values[0] is not None:
222+
self.search(ordered_list.values[0].vertex)
223+
224+
# Create the A* search object and start the search
225+
astar_search = AStarSearch(graph.bucharest)
226+
astar_search.search(graph.arad)

0 commit comments

Comments
 (0)