name: BFS (Breadth-First Search)
slug: bfs
difficulty_level: 3

description: >
  Level-by-level traversal using a queue, exploring all neighbors at the current
  depth before moving deeper. This guarantees the shortest path in unweighted
  graphs and is ideal for problems requiring layer-by-layer processing.

when_to_use: |
  - Shortest path in unweighted graphs
  - Level-order tree traversal
  - Finding all nodes at distance K
  - Word ladder / transformation problems
  - Spreading simulations (rotting oranges, infection)

metaphor: |
  Imagine dropping a stone into a still pond. Ripples spread outward in concentric
  circles—first touching everything 1 meter away, then 2 meters, then 3 meters.
  BFS works the same way: it explores all nodes at distance 1 before any node at
  distance 2.

  Another analogy: searching for someone in a building. You check everyone on your
  current floor before taking the stairs to the next floor. You never go upstairs
  until you've searched every room on the current level.

core_concept: |
  BFS uses a **queue** (FIFO) to process nodes in the order they were discovered.
  This guarantees that when you first reach a node, you've taken the shortest path
  to get there (in terms of number of edges).

  The key insight is that the queue naturally separates nodes by their distance
  from the source. Everything added in round 1 is at distance 1. Everything added
  in round 2 is at distance 2. By the time you dequeue a node, all closer nodes
  have already been processed.

  **Why it finds shortest paths**: If there were a shorter path to node X, you
  would have discovered X earlier (through that shorter path) and already
  processed it. The first time you reach X is always via the shortest route.

visualization: |
  **Example: Shortest path from A to F**

  ```
  Graph:
       A --- B --- E
       |     |
       C --- D --- F

  BFS from A:

  Queue: [A]              Visited: {A}        Level 0
         Process A → add B, C

  Queue: [B, C]           Visited: {A,B,C}    Level 1
         Process B → add E, D
         Process C → add D (skip, visited)

  Queue: [E, D]           Visited: {A,B,C,E,D} Level 2
         Process E → no new neighbors
         Process D → add F

  Queue: [F]              Visited: {A,B,C,E,D,F} Level 3
         Process F → Found! Distance = 3
  ```

  **Level-order tree traversal:**

  ```
       1
      / \
     2   3
    / \   \
   4   5   6

  Level 0: [1]
  Level 1: [2, 3]
  Level 2: [4, 5, 6]

  Result: [[1], [2,3], [4,5,6]]
  ```

code_template: |
  from collections import deque

  def bfs_shortest_path(graph: dict, start: str, end: str) -> int:
      """Find shortest path distance in unweighted graph."""
      if start == end:
          return 0

      queue = deque([start])
      visited = {start}
      distance = 0

      while queue:
          distance += 1
          # Process all nodes at current level
          for _ in range(len(queue)):
              node = queue.popleft()

              for neighbor in graph[node]:
                  if neighbor == end:
                      return distance
                  if neighbor not in visited:
                      visited.add(neighbor)
                      queue.append(neighbor)

      return -1  # No path found


  def bfs_level_order(root) -> list[list]:
      """Level-order traversal of binary tree."""
      if not root:
          return []

      result = []
      queue = deque([root])

      while queue:
          level = []
          for _ in range(len(queue)):
              node = queue.popleft()
              level.append(node.val)

              if node.left:
                  queue.append(node.left)
              if node.right:
                  queue.append(node.right)

          result.append(level)

      return result


  def bfs_matrix(grid: list[list[int]], start: tuple) -> int:
      """BFS on a 2D grid."""
      rows, cols = len(grid), len(grid[0])
      queue = deque([start])
      visited = {start}
      directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
      distance = 0

      while queue:
          for _ in range(len(queue)):
              r, c = queue.popleft()

              for dr, dc in directions:
                  nr, nc = r + dr, c + dc
                  if (0 <= nr < rows and 0 <= nc < cols
                      and (nr, nc) not in visited
                      and grid[nr][nc] != 0):  # 0 = wall
                      visited.add((nr, nc))
                      queue.append((nr, nc))

          distance += 1

      return distance

recognition_signals:
  - "shortest path"
  - "minimum steps"
  - "level order"
  - "nearest"
  - "unweighted graph"
  - "distance"
  - "spreading"
  - "layer by layer"
  - "all nodes at distance K"
  - "word ladder"
  - "rotting oranges"

common_mistakes:
  - title: Adding to visited after dequeuing instead of when enqueuing
    description: |
      Marking nodes as visited when you dequeue them (instead of when you
      enqueue them) causes the same node to be added multiple times, leading
      to incorrect distances and wasted processing.
    fix: |
      Always mark nodes as visited immediately when adding to the queue:
      ```python
      if neighbor not in visited:
          visited.add(neighbor)  # Mark NOW
          queue.append(neighbor)
      ```

  - title: Not tracking levels correctly
    description: |
      Processing the entire queue without tracking level boundaries makes it
      impossible to know the distance or handle level-by-level logic.
    fix: |
      Use the "process current level size" pattern:
      ```python
      for _ in range(len(queue)):  # Fixed size at level start
          node = queue.popleft()
          # Process node
      distance += 1  # Increment after level completes
      ```

  - title: Using a list instead of deque
    description: |
      Using `list.pop(0)` is O(n) because all elements must shift. This turns
      O(V+E) BFS into O(V²).
    fix: |
      Use `collections.deque` which has O(1) popleft:
      ```python
      from collections import deque
      queue = deque([start])
      ```

  - title: Forgetting to check bounds in grid BFS
    description: |
      Moving to invalid coordinates (negative or beyond grid dimensions) causes
      index errors.
    fix: |
      Always validate coordinates before accessing the grid:
      ```python
      if 0 <= nr < rows and 0 <= nc < cols:
          # Safe to access grid[nr][nc]
      ```

variations:
  - name: Shortest path (unweighted)
    description: |
      Classic BFS to find minimum number of edges between two nodes.
    example: "Word Ladder, Minimum Genetic Mutation, Open the Lock"

  - name: Level-order traversal
    description: |
      Process tree nodes level by level, returning grouped results.
    example: "Binary Tree Level Order Traversal, Zigzag Level Order"

  - name: Multi-source BFS
    description: |
      Start BFS from multiple sources simultaneously. Useful for "spreading"
      problems where multiple starting points expand in parallel.
    example: "Rotting Oranges, Walls and Gates, 01 Matrix"

  - name: Bidirectional BFS
    description: |
      Search from both start and end simultaneously, meeting in the middle.
      Reduces search space from O(b^d) to O(b^(d/2)).
    example: "Word Ladder (optimized), Shortest Path in large graphs"

  - name: BFS with state
    description: |
      Track additional state beyond position (e.g., keys collected, walls broken).
      State becomes part of the "node" being visited.
    example: "Shortest Path in Grid with Obstacles Elimination"

related_patterns:
  - dfs
  - tree-traversal
  - matrix-traversal

prerequisite_patterns: []
