import heapq
from collections import defaultdict
def ucs_dijkstra(graph, start, goal=None):
"""
Uniform-Cost Search / Dijkstra's Algorithm implementation
Args:
graph: Dictionary where keys are nodes and values are lists of (neighbor, cost) tuples
start: Starting node
goal: Goal node (None for full Dijkstra to all nodes)
Returns:
If goal specified: (path, total_cost) or (None, float('inf')) if no path
If goal is None: (distances, previous) for all reachable nodes
"""
# Priority queue: (cost, node, path)
priority_queue = [(0, start, [start])]
# Best known distances to each node
distances = defaultdict(lambda: float('inf'))
distances[start] = 0
# For path reconstruction
previous = {}
visited = set()
print(f"š Starting UCS/Dijkstra from {start}")
print(f"š Initial state: distances = {dict(distances)}")
print(f"š Priority queue: {priority_queue}")
print()
while priority_queue:
# Get node with minimum cost (PRIORITY DEQUEUE)
current_cost, current_node, current_path = heapq.heappop(priority_queue)
print(f"š Processing: {current_node} (cost: {current_cost})")
print(f"š Current path: {' ā '.join(current_path)}")
# Skip if we've already processed this node with a better cost
if current_node in visited:
print(f"āļø {current_node} already visited with better cost, skipping")
continue
# Mark as visited
visited.add(current_node)
# If this is our goal, we're done (for UCS)
if goal and current_node == goal:
print(f"šÆ Goal {goal} reached!")
print(f"š Optimal path: {' ā '.join(current_path)}")
print(f"š° Total cost: {current_cost}")
return current_path, current_cost
# Explore neighbors
neighbors = graph.get(current_node, [])
print(f"š Exploring neighbors of {current_node}: {[n[0] for n in neighbors]}")
for neighbor, edge_cost in neighbors:
new_cost = current_cost + edge_cost
# If we found a better path to this neighbor
if new_cost < distances[neighbor]:
distances[neighbor] = new_cost
previous[neighbor] = current_node
new_path = current_path + [neighbor]
# Add to priority queue (PRIORITY ENQUEUE)
heapq.heappush(priority_queue, (new_cost, neighbor, new_path))
print(f" ā Updated {neighbor}: cost {new_cost} via {current_node}")
else:
print(f" ā« {neighbor}: cost {new_cost} not better than {distances[neighbor]}")
print(f"š Current distances: {dict(distances)}")
print(f"š Priority queue size: {len(priority_queue)}")
print("-" * 50)
if goal:
print(f"ā No path found to {goal}")
return None, float('inf')
else:
print(f"ā
Dijkstra complete! Final distances: {dict(distances)}")
return distances, previous
def reconstruct_path(previous, start, goal):
"""Reconstruct path from previous pointers"""
path = []
current = goal
while current is not None:
path.append(current)
current = previous.get(current)
path.reverse()
return path if path[0] == start else []
# 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("=" * 60)
print("š UNIFORM-COST SEARCH: RIYADH TO MAKKAH")
print("=" * 60)
path, cost = ucs_dijkstra(saudi_cities, 'Riyadh', 'Makkah')
print(f"\nš 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 Visited: {len(path)}")
else:
print(f"ā No path found")