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.
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:
- We start from an unvisited cell and mark it as visited
- We explore its neighbors that have the same character value
- For each neighbor, we continue exploring recursively
- 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 EvaluatorExample 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:
- The
vis
array which stores boolean values for allm Γ n
cells:O(m Γ n)
- The queue/stack
q
which in the worst case (like a spiral pattern) could contain up toO(m Γ n)
elements - The
dirs
tuple and other variables useO(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.
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
Want a Structured Path to Master System Design Too? Donβt Miss This!