DFS Programming Tutorial

Understanding Stack Operations & Implementation

From Riyadh to Makkah: Programming Depth-First Search

Learning Objectives

🎯 Programming Concepts:
  • Stack data structure (LIFO)
  • Push and Pop operations
  • DFS algorithm implementation
  • Graph traversal strategies
πŸ› οΈ Practical Skills:
  • Reading and writing DFS code
  • Understanding depth-first exploration
  • Debugging recursive algorithms
  • Stack vs Queue comparison
Our 5-City Network
Riyadh ↔ Qassim (408km) Riyadh ↔ Jeddah (849km) Qassim ↔ Medina (320km) Medina ↔ Jeddah (420km) Jeddah ↔ Makkah (80km)
DFS will explore: Riyadh β†’ Qassim β†’ Medina β†’ Jeddah β†’ Makkah (depth-first)
Interactive City Map
Geographic layout shows Saudi Arabia cities
Start Cities Goal

DFS Stack Visualization

Stack is empty - Click "Start DFS" to begin!
Stack follows LIFO (Last In, First Out) principle
Push (add to top) | Pop (remove from top)
Processed items shown grayed with checkmark
Ready to start | Use Previous/Next to navigate step-by-step
πŸ”‘ Key Programming Concepts
Stack Operations:
  • stack = [start] - Initialize stack
  • stack.pop() - Pop (LIFO)
  • stack.append() - Push (add to top)
  • len(stack) - Check if empty
Data Structures:
  • list - Simple stack implementation
  • set - Fast lookup for visited cities
  • dict - Parent mapping for path reconstruction
DFS Properties:
  • Explores depth-first (go deep before wide)
  • May not find shortest path
  • Time: O(V + E), Space: O(V)
  • Uses less memory than BFS
Common Pitfalls:
  • Infinite loops without visited tracking
  • Stack overflow in recursive implementation
  • Order matters - may miss optimal paths
Complete Python Implementation
def dfs_search(graph, start, goal):
    """
    Find a path using DFS (Depth-First Search)
    
    Args:
        graph: dict of city connections {city: [neighbors]}
        start: starting city name (string)
        goal: destination city name (string)
        
    Returns:
        list: path from start to goal (may not be shortest)
        None: if no path exists
        
    Time Complexity: O(V + E) where V=vertices, E=edges
    Space Complexity: O(V) for stack and visited set
    """
    # Step 1: Initialize data structures
    stack = [start]              # LIFO stack for DFS
    visited = set([start])       # Track visited cities
    parent = {start: None}       # Track parent for path reconstruction
    
    print(f"πŸš€ Starting DFS from {start} to {goal}")
    print(f"πŸ“š Initial stack: {stack}")
    
    # Step 2: DFS main loop
    while stack:
        # Pop: Remove last city from stack (LIFO)
        current = stack.pop()
        print(f"\nπŸ” Processing: {current}")
        print(f"πŸ“š Stack after pop: {stack}")
        
        # Step 3: Check if goal reached
        if current == goal:
            print(f"🎯 Goal {goal} found!")
            # Reconstruct path by following parent pointers
            path = []
            temp = current
            while temp is not None:
                path.append(temp)
                temp = parent[temp]
            return path[::-1]  # Reverse to get start→goal order
        
        # Step 4: Explore all neighbors of current city
        neighbors = graph.get(current, [])
        print(f"πŸ‘€ Exploring neighbors of {current}: {neighbors}")
        
        # Add neighbors to stack (in reverse order for consistent exploration)
        for neighbor in reversed(neighbors):
            if neighbor not in visited:
                # Mark as visited to avoid cycles
                visited.add(neighbor)
                # Set parent for path reconstruction
                parent[neighbor] = current
                # Push: Add to top of stack
                stack.append(neighbor)
                print(f"  βž• Pushed {neighbor} to stack (via {current})")
        
        print(f"πŸ“š Stack after exploring {current}: {stack}")
        print(f"βœ… Visited cities: {visited}")
    
    print(f"❌ No path found from {start} to {goal}")
    return None  # No path exists

def dfs_recursive(graph, current, goal, visited=None, parent=None, path=None):
    """
    Recursive DFS implementation (alternative approach)
    
    Args:
        graph: dict of city connections
        current: current city being processed
        goal: destination city
        visited: set of visited cities (for recursion)
        parent: parent mapping (for recursion)  
        path: current path being built (for recursion)
        
    Returns:
        list: path from start to goal, or None if no path
    """
    if visited is None:
        visited = set()
    if parent is None:
        parent = {}
    if path is None:
        path = []
    
    # Mark current as visited
    visited.add(current)
    path.append(current)
    
    print(f"πŸ” Visiting {current}, Path so far: {path}")
    
    # Check if goal reached
    if current == goal:
        print(f"🎯 Goal {goal} found via recursion!")
        return path.copy()
    
    # Recursively explore neighbors
    neighbors = graph.get(current, [])
    for neighbor in neighbors:
        if neighbor not in visited:
            parent[neighbor] = current
            result = dfs_recursive(graph, neighbor, goal, visited, parent, path)
            if result:  # Path found
                return result
    
    # Backtrack: remove current from path
    path.pop()
    print(f"πŸ”™ Backtracking from {current}")
    
    return None  # No path found in this branch

def print_dfs_analysis(graph, start, goal):
    """Run DFS with detailed analysis"""
    print("="*60)
    print("🧠 DFS ALGORITHM ANALYSIS")
    print("="*60)
    
    print("\n--- ITERATIVE DFS ---")
    path_iterative = dfs_search(graph, start, goal)
    
    print("\n--- RECURSIVE DFS ---")
    path_recursive = dfs_recursive(graph, start, goal)
    
    print("\n--- COMPARISON ---")
    if path_iterative:
        print(f"πŸ† Iterative DFS Path: {' β†’ '.join(path_iterative)}")
        print(f"πŸ“ Path length: {len(path_iterative) - 1} hops")
    else:
        print("πŸ’” Iterative DFS: No path found")
        
    if path_recursive:
        print(f"πŸ† Recursive DFS Path: {' β†’ '.join(path_recursive)}")
        print(f"πŸ“ Path length: {len(path_recursive) - 1} hops")
    else:
        print("πŸ’” Recursive DFS: No path found")

# Our Saudi cities network (same as BFS demo)
cities_graph = {
    'Riyadh': ['Qassim', 'Jeddah'],      # Capital city: 2 connections
    'Qassim': ['Riyadh', 'Medina'],      # Central region: 2 connections  
    'Medina': ['Qassim', 'Jeddah'],      # Holy city: 2 connections
    'Jeddah': ['Riyadh', 'Medina', 'Makkah'],  # Port city: 3 connections
    'Makkah': ['Jeddah']                 # Holy city: 1 connection
}

# Example usage
if __name__ == "__main__":
    print_dfs_analysis(cities_graph, 'Riyadh', 'Makkah')
    
    # Try different start/goal combinations
    print("\n" + "="*40)
    print("πŸ”„ TESTING OTHER ROUTES")
    print("="*40)
    
    test_cases = [
        ('Riyadh', 'Medina'),
        ('Qassim', 'Makkah'),  
        ('Medina', 'Riyadh')
    ]
    
    for start, goal in test_cases:
        path = dfs_search(cities_graph, start, goal)
        result = f"{' β†’ '.join(path)}" if path else "No path"
        print(f"πŸ“ {start} to {goal}: {result}")
Code Execution Output

This shows exactly what you'll see when running the Python code:

$ python dfs_search.py

============================================================
🧠 DFS ALGORITHM ANALYSIS
============================================================
πŸš€ Starting DFS from Riyadh to Makkah
πŸ“š Initial stack: ['Riyadh']

πŸ” Processing: Riyadh
πŸ“š Stack after pop: []
πŸ‘€ Exploring neighbors of Riyadh: ['Qassim', 'Jeddah']
  βž• Pushed Jeddah to stack (via Riyadh)
  βž• Pushed Qassim to stack (via Riyadh)
πŸ“š Stack after exploring Riyadh: ['Jeddah', 'Qassim']
βœ… Visited cities: {'Riyadh', 'Qassim', 'Jeddah'}

πŸ” Processing: Qassim
πŸ“š Stack after pop: ['Jeddah']
πŸ‘€ Exploring neighbors of Qassim: ['Riyadh', 'Medina']
  βž• Pushed Medina to stack (via Qassim)
πŸ“š Stack after exploring Qassim: ['Jeddah', 'Medina']
βœ… Visited cities: {'Riyadh', 'Qassim', 'Jeddah', 'Medina'}

πŸ” Processing: Medina
πŸ“š Stack after pop: ['Jeddah']
πŸ‘€ Exploring neighbors of Medina: ['Qassim', 'Jeddah']
πŸ“š Stack after exploring Medina: ['Jeddah']
βœ… Visited cities: {'Riyadh', 'Qassim', 'Jeddah', 'Medina'}

πŸ” Processing: Jeddah
πŸ“š Stack after pop: []
πŸ‘€ Exploring neighbors of Jeddah: ['Riyadh', 'Medina', 'Makkah']
  βž• Pushed Makkah to stack (via Jeddah)
πŸ“š Stack after exploring Jeddah: ['Makkah']
βœ… Visited cities: {'Riyadh', 'Qassim', 'Jeddah', 'Medina', 'Makkah'}

πŸ” Processing: Makkah
πŸ“š Stack after pop: []
🎯 Goal Makkah found!

πŸ† SUCCESS! Path found: Riyadh β†’ Qassim β†’ Medina β†’ Jeddah β†’ Makkah
πŸ“ Path length: 4 hops
πŸ™οΈ  Cities in path: 5
Key Observations
  • LIFO Order: Qassim processed before Jeddah (added last)
  • Depth-First: Goes deep into Qassimβ†’Medina before Jeddah
  • Longer Path: Found 4-hop path (vs BFS 2-hop path)
  • Different Order: Explores branches completely
Programming Insights
  • Stack Operations: Clear push/pop pattern
  • Memory Efficient: Uses less memory than BFS
  • Path Quality: May not find shortest path
  • Implementation: Simpler than BFS (no deque needed)
Practice Exercise
🎯 Challenge: Modify the Code

Try these modifications to deepen your understanding:

  1. Add a counter to track how many cities DFS explores
  2. Print the stack contents at each step
  3. Implement iterative deepening DFS
  1. Compare recursive vs iterative DFS performance
  2. Add cycle detection without visited set
  3. Implement DFS with path cost tracking