Facebook Pixel

1559. Detect Cycles in 2D Grid

Problem Description

You are given a 2D grid of characters with dimensions m x n. Your task is to determine if there exists a cycle in the grid where all cells in the cycle contain the same character value.

A cycle is defined as a path that:

  • Has a length of 4 or more cells
  • Starts and ends at the same cell
  • Each move goes from the current cell to an adjacent cell (up, down, left, or right)
  • All cells in the path must have the same character value
  • You cannot immediately return to the cell you just came from (no immediate backtracking)

For example, if you move from cell (1, 1) to cell (1, 2), you cannot immediately move back to (1, 1) in the next step - this would be an invalid cycle.

The function should return true if at least one valid cycle exists anywhere in the grid, and false otherwise.

The solution uses BFS (Breadth-First Search) to explore the grid. For each unvisited cell, it starts a BFS traversal while keeping track of:

  • The parent cell (previous position) to avoid immediate backtracking
  • Visited cells to detect when we encounter a cell we've seen before
  • When we find a cell with the same value that has already been visited (and isn't the parent), we've found a cycle

The algorithm systematically checks all possible starting positions and paths to ensure no cycles are missed.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: The 2D grid can be viewed as a graph where each cell is a node, and edges connect adjacent cells with the same character value.

Is it a tree?

  • No: The problem specifically asks about finding cycles, which means the graph structure can have cycles (trees by definition don't have cycles).

Is the problem related to directed acyclic graphs?

  • No: The problem is about finding cycles in an undirected graph (cells connect bidirectionally to their neighbors).

Is the problem related to shortest paths?

  • No: We're not looking for shortest paths; we're looking for the existence of cycles.

Does the problem involve connectivity?

  • Yes: The problem involves connectivity - we need to check if cells with the same value can form a connected cycle.

Does the problem have small constraints?

  • Yes: While the grid can be up to a certain size, we're exploring locally connected components which typically have manageable constraints for DFS/backtracking.

DFS/backtracking?

  • Yes: DFS is perfect for cycle detection as we need to explore paths and track visited nodes to detect when we return to a previously visited cell.

Conclusion: The flowchart suggests using DFS/backtracking for detecting cycles in the 2D grid. DFS allows us to explore paths while maintaining the parent information to avoid immediate backtracking, and tracking visited cells to detect when we complete a cycle.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we think about finding cycles in a 2D grid, we need to understand what makes a valid cycle. A cycle means we can start from a cell and return to it by following a path of cells with the same value, without immediately backtracking.

The key insight is that cycle detection in graphs is a classic application of DFS or BFS. When we traverse the graph and encounter a cell we've already visited (that isn't our immediate parent), we've found a cycle.

Why does this work? Consider how we explore the grid:

  1. We start from an unvisited cell and mark it as visited
  2. We explore its neighbors that have the same character value
  3. For each neighbor, we continue exploring recursively
  4. If during exploration we reach a cell that's already been visited AND it's not the cell we just came from, we've completed a cycle

The critical part is tracking the parent cell (where we came from). Without this, we might incorrectly identify going back and forth between two cells as a cycle. For example, moving from (1,1) β†’ (1,2) β†’ (1,1) is not a valid cycle because it's just backtracking.

We need to check every unvisited cell as a potential starting point because cycles might exist in disconnected components of the grid. Each component with the same character value needs to be explored separately.

The algorithm naturally handles the "length 4 or more" requirement because:

  • We can't immediately go back (parent check)
  • To return to the starting cell, we must go through at least 2 other cells
  • This gives us: start β†’ cell1 β†’ cell2 β†’ start (minimum 4 cells including return)

Whether we use DFS or BFS doesn't fundamentally change the approach - both can detect cycles by tracking visited cells and parent relationships. The solution uses BFS with a queue, but DFS with recursion would work equally well.

Learn more about Depth-First Search, Breadth-First Search and Union Find patterns.

Solution Approach

The solution implements a BFS approach to detect cycles in the 2D grid. Let's walk through the implementation step by step:

1. Initialize the Grid Traversal:

m, n = len(grid), len(grid[0])
vis = [[False] * n for _ in range(m)]
dirs = (-1, 0, 1, 0, -1)
  • Create a vis matrix to track visited cells
  • Define dirs for the four directions (up, right, down, left) using a clever pairwise technique

2. Iterate Through All Cells:

for i, row in enumerate(grid):
    for j, x in enumerate(row):
        if vis[i][j]:
            continue
  • Check each cell as a potential starting point for a new component
  • Skip already visited cells since they've been explored in previous BFS traversals

3. BFS Implementation:

vis[i][j] = True
q = [(i, j, -1, -1)]
  • Mark the starting cell as visited
  • Initialize queue with (current_x, current_y, parent_x, parent_y)
  • Parent coordinates are set to (-1, -1) for the starting cell

4. Process the Queue:

while q:
    x, y, px, py = q.pop()
    for dx, dy in pairwise(dirs):
        nx, ny = x + dx, y + dy
  • Pop from the queue (using it as a stack for DFS-like behavior)
  • Generate next coordinates using pairwise(dirs) which creates pairs like (-1, 0), (0, 1), (1, 0), (0, -1)

5. Validate and Process Neighbors:

if 0 <= nx < m and 0 <= ny < n:
    if grid[nx][ny] != grid[i][j] or (nx == px and ny == py):
        continue
    if vis[nx][ny]:
        return True
    vis[nx][ny] = True
    q.append((nx, ny, x, y))
  • Check if the next cell is within bounds
  • Skip if it has a different value or if it's the parent cell (avoiding immediate backtracking)
  • Critical cycle detection: If we encounter a visited cell that's not our parent and has the same value, we've found a cycle
  • Otherwise, mark the cell as visited and add it to the queue with current cell as parent

6. Return Result:

  • If we complete all BFS traversals without finding a cycle, return False

The algorithm ensures that each connected component of same-valued cells is explored exactly once. The parent tracking mechanism prevents false cycle detection from immediate backtracking, while the visited matrix helps identify true cycles when we re-encounter a previously visited cell through a different path.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate how the BFS solution detects cycles.

Consider this 3x3 grid:

a a b
a a b
b b b

Step 1: Start at (0,0) with value 'a'

  • Mark (0,0) as visited
  • Queue: [(0,0, parent:(-1,-1))]
  • Visited: {(0,0)}

Step 2: Process (0,0)

  • Check neighbors with value 'a':
    • Right (0,1): Same value 'a', not visited β†’ Add to queue
    • Down (1,0): Same value 'a', not visited β†’ Add to queue
  • Mark both as visited
  • Queue: [(0,1, parent:(0,0)), (1,0, parent:(0,0))]
  • Visited: {(0,0), (0,1), (1,0)}

Step 3: Process (1,0)

  • Check neighbors with value 'a':
    • Up (0,0): Same value 'a', but it's the parent β†’ Skip
    • Right (1,1): Same value 'a', not visited β†’ Add to queue
  • Mark (1,1) as visited
  • Queue: [(0,1, parent:(0,0)), (1,1, parent:(1,0))]
  • Visited: {(0,0), (0,1), (1,0), (1,1)}

Step 4: Process (1,1)

  • Check neighbors with value 'a':
    • Up (0,1): Same value 'a', already visited, NOT the parent β†’ CYCLE FOUND!

The cycle is: (0,0) β†’ (0,1) β†’ (1,1) β†’ (1,0) β†’ (0,0)

This forms a valid cycle because:

  • All cells have the same value 'a'
  • The path has 4 cells
  • We never immediately backtrack (parent check prevents this)
  • We return to a previously visited cell through a different path

The algorithm returns true immediately upon detecting this cycle.

Key observations:

  • The parent tracking prevented us from incorrectly identifying (1,0) β†’ (0,0) β†’ (1,0) as a cycle
  • The visited set helped us detect when we reached (0,1) from a different path
  • Each 'a' cell was explored exactly once as part of this connected component

Solution Implementation

1class Solution:
2    def containsCycle(self, grid: List[List[str]]) -> bool:
3        # Get grid dimensions
4        rows, cols = len(grid), len(grid[0])
5      
6        # Track visited cells
7        visited = [[False] * cols for _ in range(rows)]
8      
9        # Direction vectors for moving up, right, down, left
10        directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]
11      
12        # Check each cell as a potential starting point
13        for start_row in range(rows):
14            for start_col in range(cols):
15                # Skip if already visited
16                if visited[start_row][start_col]:
17                    continue
18              
19                # Mark starting cell as visited
20                visited[start_row][start_col] = True
21              
22                # Initialize stack for DFS: (current_row, current_col, parent_row, parent_col)
23                # Parent is used to avoid going back to the cell we came from
24                stack = [(start_row, start_col, -1, -1)]
25              
26                # Perform DFS to find cycle
27                while stack:
28                    curr_row, curr_col, parent_row, parent_col = stack.pop()
29                  
30                    # Check all four adjacent cells
31                    for delta_row, delta_col in directions:
32                        next_row = curr_row + delta_row
33                        next_col = curr_col + delta_col
34                      
35                        # Check if next cell is within bounds
36                        if 0 <= next_row < rows and 0 <= next_col < cols:
37                            # Skip if different value or if it's the parent cell
38                            if (grid[next_row][next_col] != grid[start_row][start_col] or 
39                                (next_row == parent_row and next_col == parent_col)):
40                                continue
41                          
42                            # If we've visited this cell before in current component, we found a cycle
43                            if visited[next_row][next_col]:
44                                return True
45                          
46                            # Mark as visited and add to stack
47                            visited[next_row][next_col] = True
48                            stack.append((next_row, next_col, curr_row, curr_col))
49      
50        # No cycle found
51        return False
52
1class Solution {
2    public boolean containsCycle(char[][] grid) {
3        int rows = grid.length;
4        int cols = grid[0].length;
5        boolean[][] visited = new boolean[rows][cols];
6      
7        // Direction vectors for moving up, right, down, left
8        final int[] directions = {-1, 0, 1, 0, -1};
9      
10        // Check each cell as a potential starting point for a cycle
11        for (int row = 0; row < rows; row++) {
12            for (int col = 0; col < cols; col++) {
13                if (!visited[row][col]) {
14                    // BFS to detect cycle starting from current cell
15                    Deque<int[]> queue = new ArrayDeque<>();
16                    // Store current position and parent position (x, y, parentX, parentY)
17                    queue.offer(new int[] {row, col, -1, -1});
18                    visited[row][col] = true;
19                  
20                    while (!queue.isEmpty()) {
21                        int[] current = queue.poll();
22                        int currentX = current[0];
23                        int currentY = current[1];
24                        int parentX = current[2];
25                        int parentY = current[3];
26                      
27                        // Explore all 4 directions
28                        for (int k = 0; k < 4; k++) {
29                            int nextX = currentX + directions[k];
30                            int nextY = currentY + directions[k + 1];
31                          
32                            // Check if next position is within bounds
33                            if (nextX >= 0 && nextX < rows && nextY >= 0 && nextY < cols) {
34                                // Skip if different character or if it's the parent cell
35                                if (grid[nextX][nextY] != grid[currentX][currentY] || 
36                                    (nextX == parentX && nextY == parentY)) {
37                                    continue;
38                                }
39                              
40                                // If we've visited this cell before, we found a cycle
41                                if (visited[nextX][nextY]) {
42                                    return true;
43                                }
44                              
45                                // Mark as visited and add to queue with current cell as parent
46                                queue.offer(new int[] {nextX, nextY, currentX, currentY});
47                                visited[nextX][nextY] = true;
48                            }
49                        }
50                    }
51                }
52            }
53        }
54      
55        return false;
56    }
57}
58
1class Solution {
2public:
3    bool containsCycle(vector<vector<char>>& grid) {
4        int rows = grid.size();
5        int cols = grid[0].size();
6      
7        // Track visited cells to avoid revisiting
8        vector<vector<bool>> visited(rows, vector<bool>(cols, false));
9      
10        // Direction vectors for moving up, right, down, left
11        const vector<int> directions = {-1, 0, 1, 0, -1};
12
13        // Check each cell as a potential starting point for a cycle
14        for (int row = 0; row < rows; ++row) {
15            for (int col = 0; col < cols; ++col) {
16                // Skip if this cell has already been visited
17                if (!visited[row][col]) {
18                    // BFS to detect cycle starting from current cell
19                    // Queue stores: {current_row, current_col, parent_row, parent_col}
20                    queue<array<int, 4>> bfsQueue;
21                    bfsQueue.push({row, col, -1, -1});
22                    visited[row][col] = true;
23
24                    while (!bfsQueue.empty()) {
25                        auto current = bfsQueue.front();
26                        bfsQueue.pop();
27                      
28                        int currentRow = current[0];
29                        int currentCol = current[1];
30                        int parentRow = current[2];
31                        int parentCol = current[3];
32
33                        // Explore all 4 adjacent cells
34                        for (int dir = 0; dir < 4; ++dir) {
35                            int nextRow = currentRow + directions[dir];
36                            int nextCol = currentCol + directions[dir + 1];
37                          
38                            // Check if next cell is within grid bounds
39                            if (nextRow >= 0 && nextRow < rows && 
40                                nextCol >= 0 && nextCol < cols) {
41                              
42                                // Skip if different character or if it's the parent cell
43                                if (grid[nextRow][nextCol] != grid[currentRow][currentCol] || 
44                                    (nextRow == parentRow && nextCol == parentCol)) {
45                                    continue;
46                                }
47                              
48                                // Cycle detected: found a visited cell with same character
49                                // that is not the parent
50                                if (visited[nextRow][nextCol]) {
51                                    return true;
52                                }
53                              
54                                // Mark as visited and add to queue for further exploration
55                                bfsQueue.push({nextRow, nextCol, currentRow, currentCol});
56                                visited[nextRow][nextCol] = true;
57                            }
58                        }
59                    }
60                }
61            }
62        }
63      
64        // No cycle found in the entire grid
65        return false;
66    }
67};
68
1/**
2 * Detects if there is a cycle in the grid where cells with the same value form a cycle
3 * @param grid - 2D array of strings representing the grid
4 * @returns true if a cycle exists, false otherwise
5 */
6function containsCycle(grid: string[][]): boolean {
7    const rows: number = grid.length;
8    const cols: number = grid[0].length;
9  
10    // Track visited cells to avoid revisiting
11    const visited: boolean[][] = Array.from(
12        { length: rows }, 
13        () => Array(cols).fill(false)
14    );
15  
16    // Direction vectors for moving up, right, down, left
17    const directions: number[] = [-1, 0, 1, 0, -1];
18  
19    // Check each cell as a potential starting point for a cycle
20    for (let row = 0; row < rows; row++) {
21        for (let col = 0; col < cols; col++) {
22            // Skip if cell has already been visited
23            if (!visited[row][col]) {
24                // BFS queue: stores [currentX, currentY, parentX, parentY]
25                const queue: [number, number, number, number][] = [[row, col, -1, -1]];
26                visited[row][col] = true;
27              
28                // Process all cells in the current connected component
29                for (const [currentX, currentY, parentX, parentY] of queue) {
30                    // Check all 4 adjacent directions
31                    for (let direction = 0; direction < 4; direction++) {
32                        const nextX: number = currentX + directions[direction];
33                        const nextY: number = currentY + directions[direction + 1];
34                      
35                        // Check if next position is within grid bounds
36                        if (nextX >= 0 && nextX < rows && nextY >= 0 && nextY < cols) {
37                            // Skip if different value or if it's the parent cell (avoid going back)
38                            if (grid[nextX][nextY] !== grid[currentX][currentY] || 
39                                (nextX === parentX && nextY === parentY)) {
40                                continue;
41                            }
42                          
43                            // Cycle detected: found a visited cell that's not the parent
44                            if (visited[nextX][nextY]) {
45                                return true;
46                            }
47                          
48                            // Add new cell to queue and mark as visited
49                            queue.push([nextX, nextY, currentX, currentY]);
50                            visited[nextX][nextY] = true;
51                        }
52                    }
53                }
54            }
55        }
56    }
57  
58    return false;
59}
60

Time and Space Complexity

Time Complexity: O(m Γ— n) where m is the number of rows and n is the number of columns in the grid.

Each cell in the grid is visited at most once due to the vis array tracking visited cells. When we encounter an already visited cell from the outer loops, we skip it with continue. During the BFS/DFS traversal (using a stack here), each cell is processed once when it's marked as visited. Although we check up to 4 neighbors for each cell, this is a constant factor. Therefore, the overall time complexity is O(m Γ— n).

Space Complexity: O(m Γ— n) where m is the number of rows and n is the number of columns in the grid.

The space is used for:

  1. The vis array which stores boolean values for all m Γ— n cells: O(m Γ— n)
  2. The queue/stack q which in the worst case (like a spiral pattern) could contain up to O(m Γ— n) elements
  3. The dirs tuple and other variables use O(1) space

The dominant factor is the vis array and potentially the queue, both of which are O(m Γ— n), resulting in an overall space complexity of O(m Γ— n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Parent Tracking Leading to False Positives

One of the most common mistakes is not properly tracking the parent cell, which can lead to incorrectly identifying a cycle when you simply encounter the cell you just came from.

Incorrect Implementation:

# Wrong: Not tracking parent properly
stack = [(start_row, start_col)]  # Missing parent information

while stack:
    curr_row, curr_col = stack.pop()
  
    for delta_row, delta_col in directions:
        next_row, next_col = curr_row + delta_row, curr_col + delta_col
      
        if 0 <= next_row < rows and 0 <= next_col < cols:
            if grid[next_row][next_col] == grid[start_row][start_col]:
                if visited[next_row][next_col]:  # This will trigger falsely!
                    return True

Why it fails: Without parent tracking, when you move from cell A to cell B, and then explore B's neighbors, you'll immediately encounter A again as visited, incorrectly detecting a "cycle" of length 2.

Correct Solution: Always maintain parent coordinates and skip the parent cell during neighbor exploration:

stack = [(start_row, start_col, -1, -1)]  # Include parent coords

while stack:
    curr_row, curr_col, parent_row, parent_col = stack.pop()
  
    for delta_row, delta_col in directions:
        next_row, next_col = curr_row + delta_row, curr_col + delta_col
      
        # Skip if it's the parent cell
        if next_row == parent_row and next_col == parent_col:
            continue

2. Using Global Visited Matrix Incorrectly

Another pitfall is using a single global visited matrix without resetting it for each connected component, or conversely, resetting it unnecessarily.

Incorrect Implementation:

# Wrong: Resetting visited for each starting cell
for start_row in range(rows):
    for start_col in range(cols):
        visited = [[False] * cols for _ in range(rows)]  # Reset everything!
        visited[start_row][start_col] = True
        # ... rest of BFS/DFS

Why it fails: This prevents you from skipping already-explored cells, leading to redundant work and potentially incorrect cycle detection across component boundaries.

Correct Solution: Use a single visited matrix throughout and only process unvisited cells:

visited = [[False] * cols for _ in range(rows)]  # Initialize once

for start_row in range(rows):
    for start_col in range(cols):
        if visited[start_row][start_col]:  # Skip already visited
            continue
        # Process this component

3. Mixing BFS Queue with DFS Stack Behavior

The code uses q.pop() which actually implements DFS (stack behavior) despite the variable being named q (suggesting queue). While both work for cycle detection, mixing concepts can cause confusion.

Potentially Confusing:

q = [(i, j, -1, -1)]  # Named like a queue
while q:
    x, y, px, py = q.pop()  # But used as a stack (DFS)

Clearer Implementation: Choose one approach consistently:

# For DFS (using stack):
stack = [(i, j, -1, -1)]
while stack:
    x, y, px, py = stack.pop()
  
# For BFS (using deque):
from collections import deque
queue = deque([(i, j, -1, -1)])
while queue:
    x, y, px, py = queue.popleft()

Both approaches work for cycle detection, but being consistent with naming and behavior improves code readability and reduces confusion during debugging.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More