import heapq
import math
from collections import defaultdict
def astar_search(graph, start, goal, heuristic_func):
"""
A* Search Algorithm Implementation
Args:
graph: Dictionary where keys are nodes and values are lists of (neighbor, cost) tuples
start: Starting node
goal: Goal node
heuristic_func: Function that takes a node and returns heuristic estimate to goal
Returns:
(path, total_cost) or (None, float('inf')) if no path exists
"""
print(f"š Starting A* Search: {start} ā {goal}")
print("=" * 50)
# Priority queue: (f_cost, g_cost, node, path)
# f_cost = g_cost + h_cost for priority ordering
open_list = [(0, 0, start, [start])]
# Best known g-costs (actual cost from start)
g_costs = defaultdict(lambda: float('inf'))
g_costs[start] = 0
# Closed set (visited nodes)
closed_set = set()
print(f"š§ Heuristic function: {heuristic_func.__name__}")
print(f"š Initial state:")
print(f" g({start}) = 0")
print(f" h({start}) = {heuristic_func(start)}")
print(f" f({start}) = {0 + heuristic_func(start)}")
print()
while open_list:
# Get node with lowest f-cost (PRIORITY DEQUEUE)
f_cost, g_cost, current, path = heapq.heappop(open_list)
h_cost = f_cost - g_cost
print(f"š Processing: {current}")
print(f" š Costs: g({current})={g_cost}, h({current})={h_cost}, f({current})={f_cost}")
print(f" š¤ļø Current path: {' ā '.join(path)}")
# Skip if already processed with better cost
if current in closed_set:
print(f" āļø {current} already in closed set, skipping")
continue
# Add to closed set
closed_set.add(current)
# Check if goal reached
if current == goal:
print(f"šÆ Goal {goal} reached!")
print(f"š Optimal path: {' ā '.join(path)}")
print(f"š° Total cost: {g_cost}")
print(f"šÆ Final f-cost: {f_cost}")
return path, g_cost
# Explore neighbors
neighbors = graph.get(current, [])
print(f" š Exploring neighbors: {[n[0] for n in neighbors]}")
for neighbor, edge_cost in neighbors:
if neighbor in closed_set:
print(f" āļø {neighbor} already processed, skipping")
continue
# Calculate costs
tentative_g = g_cost + edge_cost
h_neighbor = heuristic_func(neighbor)
f_neighbor = tentative_g + h_neighbor
print(f" šļø Neighbor: {neighbor}")
print(f" š g({neighbor}) = g({current}) + edge = {g_cost} + {edge_cost} = {tentative_g}")
print(f" š§ h({neighbor}) = {h_neighbor} (heuristic estimate to goal)")
print(f" ā f({neighbor}) = g + h = {tentative_g} + {h_neighbor} = {f_neighbor}")
# If this path to neighbor is better
if tentative_g < g_costs[neighbor]:
g_costs[neighbor] = tentative_g
new_path = path + [neighbor]
# Add to open list with priority f_neighbor (PRIORITY ENQUEUE)
heapq.heappush(open_list, (f_neighbor, tentative_g, neighbor, new_path))
print(f" ⨠Updated! Added to open list with f={f_neighbor}")
else:
print(f" ā« Not better than existing g={g_costs[neighbor]}")
print(f" š Open list size: {len(open_list)}")
print(f" š Closed set size: {len(closed_set)}")
print("-" * 50)
print(f"ā No path found from {start} to {goal}")
return None, float('inf')
def euclidean_heuristic_saudi(node):
"""
Straight-line distance heuristic for Saudi cities to Makkah
Based on approximate geographic coordinates
"""
# Approximate coordinates (for heuristic calculation only)
coordinates = {
'Riyadh': (24.7136, 46.6753),
'Qassim': (26.3927, 43.9697),
'Medina': (24.5247, 39.5692),
'Jeddah': (21.4858, 39.1925),
'Makkah': (21.3891, 39.8579)
}
goal_coords = coordinates['Makkah']
node_coords = coordinates.get(node, goal_coords)
# Calculate Euclidean distance (converted to approximate km)
lat_diff = goal_coords[0] - node_coords[0]
lon_diff = goal_coords[1] - node_coords[1]
distance = math.sqrt(lat_diff**2 + lon_diff**2)
# Convert degrees to approximate kilometers (rough conversion)
km_distance = int(distance * 111) # 1 degree ā 111 km
return km_distance
# Example: Saudi Cities with real distances (km)
saudi_cities = {
'Riyadh': [('Qassim', 408), ('Jeddah', 849)],
'Qassim': [('Riyadh', 408), ('Medina', 320)],
'Medina': [('Qassim', 320), ('Jeddah', 420)],
'Jeddah': [('Riyadh', 849), ('Medina', 420), ('Makkah', 80)],
'Makkah': [('Jeddah', 80)]
}
if __name__ == "__main__":
print("š A* SEARCH: RIYADH TO MAKKAH WITH HEURISTICS")
print("=" * 60)
# Display heuristic values for all cities
print("š§ HEURISTIC VALUES (straight-line distance to Makkah):")
for city in saudi_cities.keys():
h_val = euclidean_heuristic_saudi(city)
print(f" h({city}) = {h_val} km")
print()
path, cost = astar_search(saudi_cities, 'Riyadh', 'Makkah', euclidean_heuristic_saudi)
print(f"\nš FINAL SUMMARY:")
print(f"š Start: Riyadh")
print(f"šÆ Goal: Makkah")
if path:
print(f"š¤ļø Optimal Path: {' ā '.join(path)}")
print(f"š° Total Cost: {cost} km")
print(f"šļø Cities in Path: {len(path)}")
print(f"š§ Algorithm: A* with Euclidean heuristic")
print(f"ā” Efficiency: Explored fewer nodes than blind search!")
else:
print(f"ā No path found")
print(f"\nš ALGORITHM INSIGHTS:")
print(f"ā
A* guarantees optimal solution when heuristic is admissible")
print(f"ā
Heuristic guides search toward goal (more efficient than UCS)")
print(f"ā
f(n) = g(n) + h(n) balances actual cost and estimated remaining cost")
print(f"š§ Better heuristic ā more efficiency (fewer nodes explored)")