GraphBFSDFSAdjacency ListConnected Components

Graph Traversals — The Complete Guide

Master BFS and DFS from first principles. Learn to model problems as graphs, choose the right traversal, handle cycles and components, and confidently solve interview questions involving connectivity, shortest paths, and grid exploration.

50 min read10 sections
01

Intuition from Scratch

Why does this pattern exist?

Arrays are linear — data sits in a row. Trees are hierarchical — data branches downward. But many real-world relationships are neither linear nor hierarchical. Cities connected by roads, people connected by friendships, web pages connected by links — these are graphs. A graph is just a set of nodes (vertices) connected by edges, with no restriction on shape. Any node can connect to any other node, and there can be cycles.

Graph traversal answers the most fundamental question: starting from a node, how do I systematically visit every reachable node exactly once? There are two strategies. BFS (Breadth-First Search) explores level by level — visit all neighbors first, then neighbors of neighbors. DFS (Depth-First Search) explores as deep as possible along one path before backtracking. Both visit every reachable node, but they do it in different orders, which makes each one better suited for different problems.

Graph traversals are the foundation for almost every graph algorithm: shortest paths, cycle detection, connected components, topological sort, bipartite checking, and more. If you can model a problem as a graph and traverse it correctly, you can solve a huge category of interview questions.

Analogies to Build Intuition

🌊

BFS — Ripples in a Pond

Drop a stone in a pond. Ripples spread outward in concentric circles — first the closest water, then the next ring, then the next. BFS works the same way: it visits all nodes at distance 1, then distance 2, then distance 3. This is why BFS finds the shortest path in unweighted graphs — it reaches each node at the earliest possible 'ripple.'

🧶

DFS — Exploring a Maze with String

You enter a maze holding a ball of string. At each fork, you pick one path and keep going deeper. When you hit a dead end, you follow the string back to the last fork and try the next path. You explore one complete branch before trying another. That's DFS — go deep first, backtrack when stuck.

🗺️

Graph as a Map

Think of a graph as a map of cities (nodes) connected by roads (edges). BFS is like sending scouts in all directions simultaneously — they spread outward evenly. DFS is like sending one scout who follows a single road as far as possible before turning back. Both eventually map the entire reachable territory.

What kind of problems does it solve?

Graph traversals are your go-to when:

  • You need to find the shortest path in an unweighted graph (BFS)
  • You need to count connected components or islands
  • You need to detect cycles in a directed or undirected graph
  • You need to check if a graph is bipartite (2-colorable)
  • You need to flood-fill a grid or explore a matrix
  • You need topological ordering of tasks with dependencies
  • The problem involves reachability — can you get from A to B?

The core insight

BFS and DFS are two ways to systematically visit every reachable node. BFS explores by distance (level by level) — use it when you need shortest paths or level-order information. DFS explores by depth (one branch at a time) — use it when you need to explore all paths, detect cycles, or work with recursion. Both use a "visited" set to avoid revisiting nodes in graphs with cycles.

02

Pattern Recognition

Recognizing when a problem is a graph problem — and choosing BFS vs DFS — is the most important skill. Many problems don't mention "graph" at all but are graph problems in disguise.

Keywords & Signals in the Problem Statement

Use Graph Traversal when you see...

  • "Number of islands" — grid = implicit graph
  • "Shortest path" in an unweighted graph → BFS
  • "Connected components" or "number of provinces"
  • "Can you reach from A to B?" — reachability
  • "Clone a graph" — traverse and copy
  • "Course schedule" — dependency graph → topological sort
  • "Bipartite" or "2-colorable" graph
  • "Flood fill" or "surrounded regions" on a grid
  • "Word ladder" — transform one word to another → BFS
  • A 2D grid where you move up/down/left/right

Don't use Graph Traversal when...

  • The problem is about a sorted array — use Binary Search
  • You need shortest path in a WEIGHTED graph — use Dijkstra or Bellman-Ford
  • The problem is about contiguous subarrays — use Sliding Window
  • The structure is a tree (no cycles) — simpler tree DFS/BFS suffices
  • You need to optimize over subsets — use DP or Backtracking
  • The problem is purely mathematical — no graph structure

BFS vs. DFS — When to Use Which

CriterionBFSDFS
Shortest path (unweighted)✅ Guaranteed — explores by distance❌ Not guaranteed — may find a longer path first
Level-order / distance info✅ Natural — processes level by level❌ Doesn't track levels naturally
Cycle detection✅ Works (check if neighbor is already visited)✅ Works (back edge = cycle in DFS tree)
Topological sort✅ Kahn's algorithm (BFS with in-degree)✅ Post-order DFS (reverse finish order)
Connected components✅ Works✅ Works (often simpler to code)
Path finding / backtracking❌ Hard to reconstruct paths✅ Natural — recursion tracks the path
Space complexityO(width) — can be large for wide graphsO(depth) — can be large for deep graphs
ImplementationQueue (iterative)Stack or recursion

The BFS vs DFS decision

Default rule: use BFS when you need shortest path or level-order info. Use DFS for everything else (connected components, cycle detection, path exploration). In grid problems, both work for counting islands — DFS is usually simpler to code. For "minimum steps to reach X," always use BFS.

03

Brute Force → Optimized Thinking

Let's walk through the thinking evolution using a classic problem: Number of Islands — given a 2D grid of '1's (land) and '0's (water), count the number of islands. An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically.

1

Brute Force — Check Every Cell Independently

O(m × n × m × n) time

For each land cell, do a full scan to determine which island it belongs to. Compare every pair of land cells to see if they're connected. This is essentially computing connected components by brute-force pair comparison — extremely slow.

Brute Force Thinkingtext
For each cell (i, j) that is '1':
  For each other cell (x, y) that is '1':
    Check if there's a path from (i,j) to (x,y)
    If yes, they're in the same island

Problem: Checking paths between all pairs is O((m×n)²) at best.
We're doing way too much redundant work.
2

Optimal — DFS/BFS Flood Fill

O(m × n) time · O(m × n) space

The key insight: when you find a land cell, explore the entire island from that cell using DFS or BFS (flood fill). Mark every cell in the island as visited. Then move on to find the next unvisited land cell — that's a new island. Each cell is visited exactly once.

DFS Flood Fill — Optimaltypescript
function numIslands(grid: string[][]): number {
  const rows = grid.length;
  const cols = grid[0].length;
  let islands = 0;

  function dfs(r: number, c: number) {
    // Out of bounds or water or already visited
    if (r < 0 || r >= rows || c < 0 || c >= cols) return;
    if (grid[r][c] !== '1') return;

    grid[r][c] = '0'; // mark visited (sink the land)

    // Explore all 4 directions
    dfs(r + 1, c);
    dfs(r - 1, c);
    dfs(r, c + 1);
    dfs(r, c - 1);
  }

  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      if (grid[r][c] === '1') {
        islands++;    // found a new island
        dfs(r, c);    // sink the entire island
      }
    }
  }

  return islands;
}
// Each cell is visited once by the outer loop and at most once by DFS.
// Total: O(m × n) time, O(m × n) stack space in worst case.

Why Does the Optimization Work?

1

Flood fill explores the entire connected component

Starting from any land cell, DFS/BFS visits every cell reachable through adjacent land. This is exactly one island. By marking cells as visited (sinking them to '0'), we ensure they're never counted again.

2

Each cell is processed exactly once

The outer loop touches each cell once. DFS/BFS also touches each cell at most once (because visited cells are skipped). Total work: O(m × n). No redundant pair comparisons.

3

The general principle

Counting connected components in any graph follows the same pattern: iterate over all nodes, and for each unvisited node, run a traversal (BFS or DFS) to mark the entire component. Increment the count for each new traversal. This works for grids, adjacency lists, and any graph representation.

The thinking process matters more than the code

In interviews, walk through this: "Each island is a connected component. I'll iterate over the grid. When I find an unvisited '1', I increment my island count and flood-fill the entire island using DFS, marking cells as visited. Each cell is processed once, so it's O(m × n)."

04

Core Templates

Graph traversal problems follow a few recurring templates. Memorize these — most problems are variations of one of them.

Template 1: BFS on a Graph (Adjacency List)

The standard BFS template. Uses a queue to process nodes level by level. Guarantees shortest path in unweighted graphs. The visited set prevents revisiting nodes in cyclic graphs.

BFS Template — Adjacency Listtypescript
function bfs(graph: Map<number, number[]>, start: number): void {
  const visited = new Set<number>();
  const queue: number[] = [start];
  visited.add(start);

  while (queue.length > 0) {
    const node = queue.shift()!; // dequeue front
    // Process node here

    for (const neighbor of graph.get(node) ?? []) {
      if (!visited.has(neighbor)) {
        visited.add(neighbor);   // mark BEFORE enqueuing
        queue.push(neighbor);
      }
    }
  }
}
// Time: O(V + E) — visit every vertex and edge once
// Space: O(V) — visited set + queue
// Key: mark visited WHEN ENQUEUING, not when dequeuing.
// This prevents the same node from being added to the queue multiple times.

Template 2: DFS on a Graph (Adjacency List)

The standard DFS template. Can be recursive or iterative (with a stack). Explores one branch fully before backtracking. Used for connected components, cycle detection, and path exploration.

DFS Template — Recursivetypescript
function dfs(
  graph: Map<number, number[]>,
  node: number,
  visited: Set<number>
): void {
  visited.add(node);
  // Process node here

  for (const neighbor of graph.get(node) ?? []) {
    if (!visited.has(neighbor)) {
      dfs(graph, neighbor, visited);
    }
  }
}

// Iterative version (explicit stack):
function dfsIterative(graph: Map<number, number[]>, start: number): void {
  const visited = new Set<number>();
  const stack: number[] = [start];

  while (stack.length > 0) {
    const node = stack.pop()!;
    if (visited.has(node)) continue;
    visited.add(node);
    // Process node here

    for (const neighbor of graph.get(node) ?? []) {
      if (!visited.has(neighbor)) {
        stack.push(neighbor);
      }
    }
  }
}
// Time: O(V + E). Space: O(V) for visited + O(V) stack depth.

Template 3: BFS/DFS on a Grid

Grids are implicit graphs — each cell is a node, and edges connect adjacent cells (up, down, left, right). The directions array and bounds checking replace the adjacency list.

Grid BFS Templatetypescript
function bfsGrid(grid: string[][], startR: number, startC: number): void {
  const rows = grid.length;
  const cols = grid[0].length;
  const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]]; // 4 directions

  const queue: [number, number][] = [[startR, startC]];
  grid[startR][startC] = '0'; // mark visited (or use a separate set)

  while (queue.length > 0) {
    const [r, c] = queue.shift()!;

    for (const [dr, dc] of dirs) {
      const nr = r + dr;
      const nc = c + dc;

      if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] === '1') {
        grid[nr][nc] = '0'; // mark visited BEFORE enqueuing
        queue.push([nr, nc]);
      }
    }
  }
}
// Grid DFS is the same but uses recursion or a stack instead of a queue.
// The directions array [1,0], [-1,0], [0,1], [0,-1] handles 4-directional movement.
// For 8-directional, add the 4 diagonals.
// When to use: Number of Islands, Flood Fill, Shortest Path in Binary Matrix,
// Rotting Oranges, Surrounded Regions

Template 4: BFS with Level Tracking (Shortest Path)

When you need the shortest distance, track levels explicitly. Process all nodes at the current distance before moving to the next distance. This is the standard pattern for "minimum steps" problems.

BFS with Level Trackingtypescript
function bfsShortestPath(graph: Map<number, number[]>, start: number, target: number): number {
  const visited = new Set<number>();
  const queue: number[] = [start];
  visited.add(start);
  let distance = 0;

  while (queue.length > 0) {
    const levelSize = queue.length; // nodes at current distance

    for (let i = 0; i < levelSize; i++) {
      const node = queue.shift()!;

      if (node === target) return distance; // found!

      for (const neighbor of graph.get(node) ?? []) {
        if (!visited.has(neighbor)) {
          visited.add(neighbor);
          queue.push(neighbor);
        }
      }
    }

    distance++; // all nodes at current distance processed
  }

  return -1; // target not reachable
}
// Key: the inner loop processes exactly one "level" (all nodes at the same distance).
// After the inner loop, increment distance.
// This guarantees the first time we reach the target, it's at the shortest distance.
// When to use: Word Ladder, Shortest Path in Binary Matrix, Rotting Oranges,
// minimum steps to reach a state

Which template to pick?

Ask yourself: (1) Do I need shortest path / minimum steps? → Template 4 (BFS with levels). (2) Is the input a grid? → Template 3. (3) Do I need to explore all paths or detect cycles? → Template 2 (DFS). (4) General graph traversal? → Template 1 (BFS) or Template 2 (DFS) — either works.

05

Variations & Sub-Patterns

Graph traversals aren't a single trick — they're the foundation for a whole family of algorithms. Here are the most common variations.

VariationTraversal StrategyClassic Example
Connected ComponentsDFS/BFS from each unvisited node; count traversalsNumber of Islands, Number of Provinces
Shortest Path (unweighted)BFS with level tracking; first visit = shortestWord Ladder, Shortest Path in Binary Matrix
Multi-source BFSStart BFS from ALL sources simultaneouslyRotting Oranges, 01 Matrix, Walls and Gates
Cycle Detection (undirected)DFS: if neighbor is visited and not parent → cycleGraph Valid Tree, Redundant Connection
Cycle Detection (directed)DFS with 3 colors: white/gray/black; gray→gray = cycleCourse Schedule, Detect Cycle in Directed Graph
Topological SortBFS (Kahn's) or DFS post-order reverseCourse Schedule II, Alien Dictionary
Bipartite CheckBFS/DFS with 2-coloring; conflict = not bipartiteIs Graph Bipartite?, Possible Bipartition
Graph CloningDFS/BFS + hash map (old node → new node)Clone Graph

Problems mentioned above

Deep Dive: Multi-Source BFS — Rotting Oranges

In a grid, every rotten orange infects adjacent fresh oranges each minute. Find the minimum time until all oranges are rotten (or -1 if impossible). The trick: start BFS from ALL rotten oranges simultaneously. Each BFS level = one minute.

Rotting Oranges — Multi-Source BFStypescript
function orangesRotting(grid: number[][]): number {
  const rows = grid.length, cols = grid[0].length;
  const queue: [number, number][] = [];
  let fresh = 0;

  // Enqueue ALL rotten oranges as starting points
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      if (grid[r][c] === 2) queue.push([r, c]);
      if (grid[r][c] === 1) fresh++;
    }
  }

  if (fresh === 0) return 0; // no fresh oranges

  const dirs = [[1,0],[-1,0],[0,1],[0,-1]];
  let minutes = 0;

  while (queue.length > 0 && fresh > 0) {
    const levelSize = queue.length;
    for (let i = 0; i < levelSize; i++) {
      const [r, c] = queue.shift()!;
      for (const [dr, dc] of dirs) {
        const nr = r + dr, nc = c + dc;
        if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] === 1) {
          grid[nr][nc] = 2; // rot it
          fresh--;
          queue.push([nr, nc]);
        }
      }
    }
    minutes++;
  }

  return fresh === 0 ? minutes : -1;
}
// Multi-source BFS: all rotten oranges spread simultaneously.
// Each BFS level = 1 minute. When fresh === 0, we're done.
// Time: O(m × n). Space: O(m × n) for the queue.

Deep Dive: Topological Sort — Course Schedule II

Given n courses and prerequisites, return a valid order to take all courses (or empty if impossible). This is topological sort on a directed graph. Kahn's algorithm (BFS) processes nodes with in-degree 0 first.

Course Schedule II — Kahn's Algorithm (BFS)typescript
function findOrder(numCourses: number, prerequisites: number[][]): number[] {
  // Build adjacency list and in-degree array
  const graph = new Map<number, number[]>();
  const inDegree = new Array(numCourses).fill(0);

  for (const [course, prereq] of prerequisites) {
    if (!graph.has(prereq)) graph.set(prereq, []);
    graph.get(prereq)!.push(course);
    inDegree[course]++;
  }

  // Start with all courses that have no prerequisites
  const queue: number[] = [];
  for (let i = 0; i < numCourses; i++) {
    if (inDegree[i] === 0) queue.push(i);
  }

  const order: number[] = [];

  while (queue.length > 0) {
    const course = queue.shift()!;
    order.push(course);

    for (const next of graph.get(course) ?? []) {
      inDegree[next]--;
      if (inDegree[next] === 0) {
        queue.push(next); // all prerequisites satisfied
      }
    }
  }

  // If we processed all courses, the order is valid
  return order.length === numCourses ? order : [];
}
// Time: O(V + E). Space: O(V + E).
// If order.length < numCourses, there's a cycle → impossible.
// Kahn's naturally detects cycles: nodes in a cycle never reach in-degree 0.

Deep Dive: Cycle Detection in Directed Graphs — 3-Color DFS

To detect cycles in a directed graph, use DFS with three states: white (unvisited), gray (in current DFS path), black (fully processed). If you encounter a gray node, you've found a back edge — a cycle.

Directed Cycle Detection — 3-Color DFStypescript
function hasCycle(numNodes: number, edges: number[][]): boolean {
  const graph = new Map<number, number[]>();
  for (const [u, v] of edges) {
    if (!graph.has(u)) graph.set(u, []);
    graph.get(u)!.push(v);
  }

  // 0 = white (unvisited), 1 = gray (in path), 2 = black (done)
  const color = new Array(numNodes).fill(0);

  function dfs(node: number): boolean {
    color[node] = 1; // gray — currently exploring

    for (const neighbor of graph.get(node) ?? []) {
      if (color[neighbor] === 1) return true;  // back edge → cycle!
      if (color[neighbor] === 0 && dfs(neighbor)) return true;
    }

    color[node] = 2; // black — fully processed
    return false;
  }

  for (let i = 0; i < numNodes; i++) {
    if (color[i] === 0 && dfs(i)) return true;
  }

  return false;
}
// Gray → gray = back edge = cycle in directed graph.
// Black nodes are safe — they've been fully explored with no cycle.
// This is the standard technique for Course Schedule (can you finish all courses?).
06

Visual Walkthrough

Let's trace through BFS and DFS on the same graph to see how they differ, then walk through a grid problem.

BFS vs DFS on the Same Graph

BFS vs DFS — Tracetext
Graph (undirected):
    013
    |   |
    2   4

Adjacency list:
  0: [1, 2]
  1: [0, 3, 4]
  2: [0]
  3: [1]
  4: [1]

BFS from node 0 (queue):
  Queue: [0]         Visit: 0Process 0, enqueue neighbors 1, 2
  Queue: [1, 2]      Visit: 1Process 1, enqueue neighbors 3, 4
  Queue: [2, 3, 4]   Visit: 2Process 2, no new neighbors
  Queue: [3, 4]      Visit: 3Process 3, no new neighbors
  Queue: [4]          Visit: 4Process 4, no new neighbors
  
  BFS order: 0, 1, 2, 3, 4  (level by level)
  Level 0: {0}
  Level 1: {1, 2}
  Level 2: {3, 4}

DFS from node 0 (recursive):
  Visit 0recurse to neighbor 1
    Visit 1recurse to neighbor 3
      Visit 3no unvisited neighbors, backtrack
    Back to 1recurse to neighbor 4
      Visit 4no unvisited neighbors, backtrack
    Back to 1backtrack
  Back to 0recurse to neighbor 2
    Visit 2no unvisited neighbors, backtrack
  
  DFS order: 0, 1, 3, 4, 2  (depth first)

Key difference:
  BFS: 0 → {1,2} → {3,4}     (explores by distance)
  DFS: 01342     (explores one branch fully before the next)

Number of Islands — Grid DFS Trace

Number of Islands — Tracetext
Grid:
  1 1 0 0 0
  1 1 0 0 0
  0 0 1 0 0
  0 0 0 1 1

Scan left-to-right, top-to-bottom:

(0,0) = '1'NEW ISLAND #1
  DFS flood fill: (0,0)→(1,0)→(1,1)→(0,1) all become '0'
  Grid after:
    0 0 0 0 0
    0 0 0 0 0
    0 0 1 0 0
    0 0 0 1 1

(0,1) to (1,1) = '0'skip

(2,2) = '1'NEW ISLAND #2
  DFS flood fill: (2,2) becomes '0'
  Grid after:
    0 0 0 0 0
    0 0 0 0 0
    0 0 0 0 0
    0 0 0 1 1

(3,3) = '1'NEW ISLAND #3
  DFS flood fill: (3,3)→(3,4) both become '0'
  Grid after:
    0 0 0 0 0
    0 0 0 0 0
    0 0 0 0 0
    0 0 0 0 0

Answer: 3 islands

Key: Each DFS call from the outer loop discovers one complete island.
Sinking land to '0' prevents double-counting.

Topological Sort — Kahn's Algorithm Trace

Topological Sort — Tracetext
Courses: 0, 1, 2, 3
Prerequisites: [[1,0], [2,0], [3,1], [3,2]]
  Meaning: 01, 02, 13, 23

In-degrees: [0, 1, 1, 2]
  Course 0: 0 prerequisites
  Course 1: 1 prerequisite (course 0)
  Course 2: 1 prerequisite (course 0)
  Course 3: 2 prerequisites (courses 1 and 2)

Step 1: Queue = [0] (in-degree 0)
  Process 0order = [0]
  Decrement neighbors: inDegree[1]=0, inDegree[2]=0
  Queue = [1, 2]

Step 2: Process 1order = [0, 1]
  Decrement neighbors: inDegree[3]=1
  Queue = [2]

Step 3: Process 2order = [0, 1, 2]
  Decrement neighbors: inDegree[3]=0
  Queue = [3]

Step 4: Process 3order = [0, 1, 2, 3]
  Queue = []

Answer: [0, 1, 2, 3] ✓ (valid topological order)
All 4 courses processedno cycle.

If a cycle existed, some nodes would never reach in-degree 0,
and order.length < numCoursesreturn [].
07

Practice Problems

These 7 problems are carefully ordered from easy to hard. Each one teaches a different graph traversal technique. Solve them in order.

1

LC 733. Flood Fill

Easy

Why this pattern:

Grid DFS/BFS (Template 3). Starting from a pixel, change all connected pixels of the same color to a new color. This is the simplest graph traversal on a grid — pure flood fill with no extra logic.

Key idea: DFS from the starting pixel. For each cell, if it matches the original color, change it to the new color and recurse on all 4 neighbors. The color change acts as the 'visited' marker. Handle the edge case where newColor === originalColor (no-op).

2

LC 200. Number of Islands

Medium

Why this pattern:

Connected components on a grid (Template 3). Each island is a connected component of '1's. Iterate over the grid; for each unvisited '1', increment the count and flood-fill the entire island. The foundational grid graph problem.

Key idea: Outer loop scans every cell. When a '1' is found, increment islands and run DFS/BFS to sink the entire island (mark all connected '1's as '0'). Each cell is visited at most once → O(m × n).

3

LC 994. Rotting Oranges

Medium

Why this pattern:

Multi-source BFS with level tracking (Template 4 variant). All rotten oranges spread simultaneously — this is BFS from multiple sources. Each BFS level = 1 minute. This teaches the critical multi-source BFS pattern.

Key idea: Enqueue ALL rotten oranges at the start. BFS level by level: each level rots adjacent fresh oranges. Track fresh count; when it hits 0, return the number of levels. If fresh > 0 after BFS, return -1.

4

LC 133. Clone Graph

Medium

Why this pattern:

Graph traversal + hash map (DFS/BFS + cloning). Traverse the graph and create a copy of each node. Use a hash map (old node → new node) to avoid duplicating nodes and to handle cycles. This teaches graph cloning with cycle handling.

Key idea: DFS: for each node, if it's already in the map, return the clone. Otherwise, create a new node, add it to the map, then recursively clone all neighbors. The map serves as both the visited set and the old→new mapping.

5

LC 210. Course Schedule II

Medium

Why this pattern:

Topological sort (Kahn's BFS — Template 4 variant). Build a directed graph from prerequisites. Use BFS with in-degree tracking to find a valid course order. If not all courses are processed, there's a cycle.

Key idea: Build adjacency list and in-degree array. Enqueue all nodes with in-degree 0. Process each node: add to order, decrement neighbors' in-degrees. When a neighbor reaches in-degree 0, enqueue it. If order.length < numCourses, cycle exists.

6

LC 127. Word Ladder

Hard

Why this pattern:

BFS shortest path on an implicit graph (Template 4). Each word is a node; edges connect words that differ by one letter. BFS finds the shortest transformation sequence. This teaches modeling non-obvious problems as graphs.

Key idea: BFS from beginWord. For each word, try changing each character to a-z. If the new word is in the word list, it's a neighbor. Use a set for O(1) lookup. BFS with level tracking gives the shortest path. Remove visited words from the set to avoid revisiting.

7

LC 417. Pacific Atlantic Water Flow

Medium

Why this pattern:

Reverse BFS/DFS from borders (multi-source). Instead of checking if each cell can reach both oceans (expensive), start from the ocean borders and flow inward. Cells reachable from both oceans are the answer. This teaches reverse-thinking in graph problems.

Key idea: Run DFS/BFS from all Pacific border cells (top row + left column). Run another from all Atlantic border cells (bottom row + right column). The intersection of both reachable sets is the answer. Reverse flow: move to neighbors with height ≥ current.

Practice strategy

For each problem: (1) Identify the graph — what are nodes and edges? (2) Choose BFS or DFS and justify why. (3) Decide on the visited strategy (modify grid, separate set, or coloring). (4) After solving, write: "This is [BFS/DFS] because [reason]. Nodes are [X], edges are [Y]."

08

Common Mistakes

These are the traps that catch people on graph traversal problems. Learn them here so you don't learn them in an interview.

🔁

Marking visited when DEQUEUING instead of ENQUEUING

In BFS, you mark a node as visited when you pop it from the queue. But by then, the same node may have been enqueued multiple times by different neighbors. This causes duplicate processing and can turn O(V+E) into O(V²).

Always mark visited WHEN ENQUEUING (before pushing to the queue), not when dequeuing. This ensures each node enters the queue exactly once. For DFS with an explicit stack, check visited when popping (since the stack may have duplicates).

🌀

Infinite loop from missing visited check

In a graph with cycles, you visit node A, which leads to B, which leads back to A. Without a visited set, you loop forever. This is the #1 bug in graph problems.

ALWAYS maintain a visited set (or modify the grid in-place). Check it before processing any neighbor. For grids, marking cells as '0' or '#' is the simplest approach. For adjacency lists, use a Set<number>.

📐

Wrong bounds checking in grid problems

You access grid[r-1][c] without checking if r-1 >= 0, causing an index-out-of-bounds error. Or you check bounds after accessing the cell instead of before.

Always check bounds FIRST: if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] === target). Use the directions array pattern: const dirs = [[1,0],[-1,0],[0,1],[0,-1]] and loop through it. This is cleaner than writing 4 separate if-statements.

🎯

Using DFS for shortest path

You use DFS to find the shortest path in an unweighted graph. DFS finds A path, but not necessarily the shortest one. It might explore a long detour before finding the direct route.

For shortest path in unweighted graphs, ALWAYS use BFS. BFS explores by distance — the first time it reaches a node is guaranteed to be the shortest path. DFS has no such guarantee.

🏗️

Building the adjacency list wrong

For undirected graphs, you add edge (u, v) but forget to add (v, u). Or for directed graphs, you add both directions when you should only add one. This corrupts the graph structure.

Undirected: add BOTH directions (u→v and v→u). Directed: add only the specified direction. Double-check by reading the problem statement: 'undirected' means bidirectional edges, 'directed' means one-way.

🔢

Forgetting disconnected components

You run BFS/DFS from node 0 and assume you've visited everything. But the graph might have multiple disconnected components. Nodes in other components are never visited.

Always loop over ALL nodes and start a traversal from each unvisited one. The pattern: for (let i = 0; i < n; i++) { if (!visited.has(i)) { bfs/dfs(i); componentCount++; } }. This handles disconnected graphs correctly.

09

Interview Strategy

Graph problems can feel overwhelming because there are many variations. Here's a structured approach to tackle them in interviews.

The 5-Step Interview Flow

1

Model the Graph

'What are the nodes? What are the edges? Is it directed or undirected? Can there be cycles?' — For grids: cells are nodes, adjacent cells are edges. For word problems: words are nodes, one-letter differences are edges. Explicitly state the graph model.

2

Choose BFS or DFS

'I need shortest path → BFS. I need to explore all paths or detect cycles → DFS. I need topological order → BFS (Kahn's) or DFS (post-order).' — Justify your choice. If either works (e.g., connected components), pick whichever you're more comfortable with.

3

Handle the Visited Strategy

'I'll use a visited set to avoid cycles. For grids, I'll mark cells in-place. For adjacency lists, I'll use a Set<number>.' — State this explicitly. Forgetting visited is the #1 graph bug.

4

Code It Clean

Build the adjacency list (if needed). Write the traversal using the appropriate template. Use the directions array for grids. Keep the code modular: separate graph building from traversal.

5

Analyze Complexity

'Time: O(V + E) — I visit every vertex and edge once. Space: O(V) for the visited set and queue/stack.' For grids: V = m×n, E = 4×m×n (each cell has at most 4 edges). Always mention both time and space.

Common Follow-Up Questions

Follow-UpHow to Handle
"Can you do it with BFS instead of DFS?" (or vice versa)Yes — for most problems, both work. BFS uses a queue, DFS uses recursion/stack. Mention the tradeoff: BFS guarantees shortest path; DFS uses less memory for deep narrow graphs.
"What if the graph is weighted?"BFS no longer gives shortest path. Use Dijkstra's (non-negative weights) or Bellman-Ford (negative weights). Mention this upgrade path even if the current problem is unweighted.
"What if the grid is very large?"DFS might cause stack overflow on huge grids (recursion depth = m×n). Use iterative DFS with an explicit stack, or BFS (which uses a queue instead of the call stack).
"How do you detect a cycle?"Undirected: if a neighbor is visited and not the parent → cycle. Directed: use 3-color DFS (white/gray/black); gray→gray = back edge = cycle. Or use Kahn's: if not all nodes are processed, there's a cycle.
"Can you do it without extra space?"For grids, modify the grid in-place (mark visited cells). For adjacency lists, you generally need O(V) space for the visited set — it's unavoidable for cycle prevention.
"What about bidirectional BFS?"For shortest path between two specific nodes, BFS from both ends simultaneously. They meet in the middle, reducing the search space from O(b^d) to O(b^(d/2)). Useful for Word Ladder.

The meta-skill

The hardest part of graph problems isn't the traversal — it's modeling the problem as a graph. Practice asking: "What are the nodes? What are the edges?" Once you have the graph model, the traversal is just a template. Word Ladder, Course Schedule, and Rotting Oranges all look different, but they're all just BFS/DFS on a graph with different node and edge definitions.

10

Summary & Cheat Sheet

Pin this section. Review it before mock interviews and contests.

Quick Revision Cheat Sheet

Core idea: BFS explores by distance (level by level, queue). DFS explores by depth (one branch fully, stack/recursion). Both visit every reachable node exactly once

BFS = shortest path: In unweighted graphs, BFS guarantees the shortest path. First visit = minimum distance. Always use BFS for 'minimum steps' problems

DFS = explore all: DFS is natural for path exploration, cycle detection, connected components, and topological sort. Recursive DFS is usually the cleanest code

Visited is mandatory: Graphs have cycles. Without a visited set, you loop forever. Mark visited WHEN ENQUEUING (BFS) or WHEN ENTERING (DFS)

Grid = implicit graph: Cells are nodes, adjacent cells are edges. Use dirs = [[1,0],[-1,0],[0,1],[0,-1]] for 4-directional movement

Multi-source BFS: Enqueue ALL sources at the start. Each BFS level = 1 time unit. Used for Rotting Oranges, 01 Matrix, Walls and Gates

Topological sort: Kahn's (BFS): process in-degree 0 nodes first. DFS: reverse post-order. Detects cycles: if not all nodes processed → cycle

Cycle detection: Undirected: visited neighbor ≠ parent → cycle. Directed: 3-color DFS, gray→gray = back edge = cycle

Complexity: Time: O(V + E). Space: O(V). For grids: V = m×n, E ≈ 4mn. Always state both

Build adjacency list: Undirected: add both u→v and v→u. Directed: add only the specified direction. Use Map<number, number[]>

Graph modeling: The hardest part. Ask: 'What are nodes? What are edges?' Word Ladder: words=nodes, 1-letter diff=edges. Course Schedule: courses=nodes, prereqs=edges

Mental trigger: "Shortest path" or "connected components" or "grid exploration" or "dependencies" → Graph Traversal (BFS or DFS)

Decision Flowchart

When to Use Graph Traversals — Decision Treetext
Can you model the problem as nodes and edges?
├── NONot a graph problem. Consider arrays, trees, or other patterns.
└── YESDo you need the SHORTEST PATH (unweighted)?
          ├── YESBFS with level tracking (Template 4)
Word Ladder, Shortest Path in Binary Matrix, Rotting Oranges
          └── NODo you need TOPOLOGICAL ORDER (dependencies)?
                    ├── YESKahn's BFS (in-degree) or DFS post-order
Course Schedule, Alien Dictionary
                    └── NODo you need to DETECT CYCLES?
                              ├── YESUndirected: DFS + parent check
Directed: 3-color DFS (graygray)
                              └── NODo you need CONNECTED COMPONENTS?
                                        ├── YESDFS/BFS from each unvisited node
Number of Islands, Number of Provinces
                                        └── NOIs it a GRID problem?
                                                  ├── YESGrid DFS/BFS (Template 3)
Flood Fill, Surrounded Regions
                                                  └── NOGeneral BFS or DFS
                                                            Clone Graph, Is Bipartite

Graph Representation Quick Reference

RepresentationSpaceEdge LookupWhen to Use
Adjacency List (Map)O(V + E)O(degree)Sparse graphs (most interview problems)
Adjacency MatrixO(V²)O(1)Dense graphs, need fast edge lookup
Edge ListO(E)O(E)Union-Find problems, Kruskal's MST
Implicit GridO(m × n)O(1)2D grid problems (no explicit graph needed)

One last thing

Graph traversals are the gateway to the most interesting problems in DSA: shortest paths, network flow, strongly connected components, and more. But at their core, BFS and DFS are simple — visit every reachable node exactly once, using a queue or a stack. The real skill is modeling the problem as a graph. Once you see the nodes and edges, the traversal is just a template. Practice the modeling step on every problem, and graph questions will stop feeling intimidating.