UCS Dictionary Approach Tutorial

Learn Uniform-Cost Search using the simple dictionary approach - easier to understand than priority queues while maintaining the same algorithm logic!

UCS Dictionary Approach - Simpler Implementation

Key Idea: Instead of using a complex priority queue, we use a simple dictionary to track unvisited nodes and their costs. We repeatedly find the minimum cost node, process it, and update neighboring costs.

Simplified Algorithm Steps:
  1. Initialize: Create dictionary with all nodes having infinite cost (except start = 0)
  2. Find minimum: Get the unvisited node with the lowest cost
  3. Process: Remove it from unvisited, mark as visited
  4. Update neighbors: Calculate new costs via current node and update if better
  5. Repeat: Continue until goal is reached or no unvisited nodes remain
Why This Approach? This dictionary method is much easier to understand and implement than priority queues, making it perfect for learning UCS concepts. The algorithm logic remains identical!
Algorithm Properties
Complete: Yes
Optimal: Yes
Time: O(V²) simple
Space: O(V)
Structure: Dictionary/Map
Dictionary Visualization
Step 0 of 0
UCS Dictionary - Track Unvisited Nodes & Costs

Click "Start UCS" to begin visualization

Current Step:
šŸŽÆ Ready to Begin UCS Dictionary Algorithm
Click "Start UCS" to see how we use a simple dictionary to find shortest paths.
šŸ“š What you'll see: Dictionary operations, cost comparisons, and path discovery.
šŸŽ“ Learning Goal: Understand UCS using the simplest possible data structure!
Interactive City Map
Geographic layout shows Saudi Arabia cities
Start Cities Goal
Python Implementation - Dictionary Approach
def ucs_dictionary_approach(graph, start, goal):
    """
    Uniform-Cost Search using simple dictionary approach
    Much easier to understand than priority queues!
    
    Args:
        graph: Dictionary where keys are nodes and values are lists of (neighbor, cost) tuples
        start: Starting node
        goal: Goal node
    
    Returns:
        (path, total_cost) or (None, float('inf')) if no path exists
    """
    
    print(f"šŸš€ Starting UCS Dictionary Approach: {start} → {goal}")
    print("=" * 50)
    
    # Initialize: All nodes start with infinite cost except start node
    unvisited = {node: float('inf') for node in graph.keys()}
    unvisited[start] = 0
    
    # Track visited nodes and parent pointers for path reconstruction
    visited = set()
    parent = {start: None}
    
    print(f"šŸ“‹ Initial unvisited dictionary: {dict(unvisited)}")
    print(f"šŸŽÆ Looking for path to: {goal}")
    print()
    
    while unvisited:
        # STEP 1: Find the unvisited node with minimum cost (simple linear search)
        current = min(unvisited, key=unvisited.get)
        current_cost = unvisited[current]
        
        print(f"šŸ” Step: Finding minimum cost node from unvisited")
        print(f"šŸ“Š Unvisited nodes: {dict(unvisited)}")
        print(f"⭐ Selected: {current} with cost {current_cost}")
        
        # If we reached an unreachable node (infinite cost), no path exists
        if current_cost == float('inf'):
            print(f"āŒ No path to {goal} exists (all remaining nodes unreachable)")
            return None, float('inf')
        
        # STEP 2: Remove current node from unvisited (mark as processed)
        del unvisited[current]
        visited.add(current)
        
        print(f"āœ… Moved {current} from unvisited to visited")
        print(f"šŸ—‚ļø  Visited nodes: {visited}")
        
        # STEP 3: Check if we reached the goal
        if current == goal:
            path = reconstruct_path(parent, start, goal)
            print(f"šŸŽÆ Goal {goal} reached!")
            print(f"šŸ† Optimal path: {' → '.join(path)}")
            print(f"šŸ’° Total cost: {current_cost}")
            return path, current_cost
        
        # STEP 4: Update costs for all neighbors
        print(f"šŸ‘€ Examining neighbors of {current}:")
        neighbors = graph.get(current, [])
        
        for neighbor, edge_cost in neighbors:
            if neighbor not in visited:  # Only check unvisited neighbors
                new_cost = current_cost + edge_cost
                old_cost = unvisited.get(neighbor, float('inf'))
                
                print(f"   šŸ™ļø  Neighbor: {neighbor}")
                print(f"      šŸ“ Path cost via {current}: {current_cost} + {edge_cost} = {new_cost}")
                print(f"      šŸ“Š Current best cost for {neighbor}: {old_cost}")
                
                # Update if we found a better path
                if new_cost < old_cost:
                    unvisited[neighbor] = new_cost
                    parent[neighbor] = current
                    print(f"      ✨ Updated! New best cost for {neighbor}: {new_cost}")
                else:
                    print(f"      ⚫ No improvement (kept {old_cost})")
            else:
                print(f"   ā­ļø  {neighbor}: already visited, skipping")
        
        print(f"šŸ“‹ Updated unvisited dictionary: {dict(unvisited)}")
        print("-" * 50)
    
    # If we exit the loop without finding goal
    print(f"āŒ Goal {goal} not reachable from {start}")
    return None, float('inf')

def reconstruct_path(parent, start, goal):
    """Reconstruct path from parent pointers"""
    path = []
    current = goal
    while current is not None:
        path.append(current)
        current = parent.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("šŸ•Œ UCS DICTIONARY APPROACH: RIYADH TO MAKKAH")
    print("=" * 60)
    
    path, cost = ucs_dictionary_approach(saudi_cities, 'Riyadh', 'Makkah')
    
    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 Visited: {len(path)}")
        print(f"šŸŽ“ Algorithm: Simple Dictionary UCS (Easy to understand!)")
    else:
        print(f"āŒ No path found")
    
    print("\nšŸ”¬ ALGORITHM ANALYSIS:")
    print("āœ… Uses simple dictionary operations (no complex data structures)")
    print("āœ… Easy to debug and understand step-by-step")
    print("āœ… Same optimal results as priority queue version")
    print("ā° Time complexity: O(V²) for simple graphs")
    print("šŸ’¾ Space complexity: O(V) for storing costs and visited nodes")
Code Execution Output
Python 3.x Terminal ucs_dictionary_demo.py
Click "Run Code" to execute UCS Dictionary algorithm and see step-by-step output.
Dictionary vs Priority Queue
Dictionary Approach (This Tutorial)
  • āœ… Simpler to understand: Just find minimum, remove, update
  • āœ… Easy to debug: See all costs at any time
  • āœ… No complex data structures: Basic dictionary operations
  • āœ… Perfect for learning: Crystal clear algorithm steps
  • āš ļø Less efficient: O(V²) time complexity
Priority Queue Approach
  • āœ… More efficient: O((V+E) log V) time
  • āœ… Scalable: Better for large graphs
  • āš ļø More complex: Requires heap operations
  • āš ļø Harder to debug: Internal heap structure
Learning Benefits
  • Clear Visualization: See exactly which nodes are unvisited and their costs
  • Step-by-step Logic: Every operation is transparent and understandable
  • Easy Implementation: Can be coded in any language with basic data structures
  • Algorithm Understanding: Focus on UCS logic rather than data structure complexity
  • Debug Friendly: Print the dictionary at any step to see algorithm state
  • Foundation Building: Perfect stepping stone before learning heap-based versions
Practice Exercise
Challenge: Implement Dictionary UCS Yourself!

Try these progressive exercises to master the dictionary approach:

Beginner Level:
  • Trace through the algorithm manually with pen and paper
  • Implement the dictionary approach in your favorite language
  • Add print statements to see the dictionary at each step
  • Test with different start and goal nodes
Advanced Level:
  • Compare performance with priority queue implementation
  • Modify to find all shortest paths to all nodes (like Dijkstra)
  • Add path reconstruction and visualization
  • Handle negative edge weights (requires different approach)
Pro Tip: Once you master this dictionary approach, the priority queue version will make much more sense! The algorithm logic is identical - only the data structure for finding the minimum changes.