Detect Cycles in 2D Grid
MedPrompt
Given a 2D array of characters grid of size m x n, you need to find if there exists any cycle consisting of the same value in grid.
A cycle is a path of length 4 or more in the grid that starts and ends at the same cell. From a given cell, you can move to one of the cells adjacent to it - in one of the four directions (up, down, left, or right), if it has the same value of the current cell.
Also, you cannot move to the cell that you visited in your last move. For example, the cycle (1, 1) -> (1, 2) -> (1, 1) is invalid because from (1, 2) we visited (1, 1) which was the last visited cell.
Return true if any cycle of the same value exists in grid, otherwise, return false.
Example 1:

Input: grid = [["a","a","a","a"],["a","b","b","a"],["a","b","b","a"],["a","a","a","a"]]
Output: true
Explanation: There are two valid cycles shown in different colors in the image below:Example 2:

Input: grid = [["c","c","c","a"],["c","d","c","c"],["c","c","e","c"],["f","c","c","c"]]
Output: true
Explanation: There is only one valid cycle highlighted in the image below:Example 3:

Input: grid = [["a","b","b"],["b","z","b"],["b","b","a"]]
Output: false
Constraints:
m == grid.lengthn == grid[i].length1 <= m, n <= 500gridconsists only of lowercase English letters.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach models the grid as an undirected graph. Each cell (r, c) is a node, and an edge exists between two adjacent cells if they contain the same character. The problem then becomes finding a cycle in this graph. We can use Depth First Search (DFS) to traverse the graph and detect cycles. We iterate through every cell of the grid. If a cell hasn't been visited, we start a DFS from it. During the traversal, if we encounter a visited node that is not the immediate parent of the current node in the DFS tree, we have found a back edge, which indicates a cycle.
Algorithm
- Initialize a
visited[m][n]2D boolean array, marking all cells as not visited. - Iterate through each cell
(r, c)of the grid from(0, 0)to(m-1, n-1). - If the current cell
(r, c)has not been visited (visited[r][c]is false), it signifies the start of a new, unexplored connected component. - Start a Depth First Search (DFS) from this cell. The DFS function will be of the form
dfs(row, col, parent_row, parent_col). - The
dfsfunction:- Marks the current cell
(row, col)as visited. - Iterates through its four neighbors (up, down, left, right).
- For each neighbor
(nr, nc): a. It must be within the grid boundaries. b. It must have the same character as the starting cell of the current component. c. It must not be the immediate parent cell from which we arrived at(row, col). This prevents trivial 1-edge-back cycles. d. If the neighbor(nr, nc)has already been visited (and is not the parent), a cycle has been detected. Returntrue. e. If the neighbor is valid but not visited, make a recursive call:dfs(nr, nc, row, col). If this call returnstrue, propagate the result by returningtrueimmediately.
- Marks the current cell
- If the DFS completes for a component without finding a cycle, it returns
false. - If the main loop finishes checking all cells and components without any DFS returning
true, then no cycles exist in the grid. Returnfalse.
Walkthrough
To implement this, we'll use a 2D boolean array, visited, of the same size as the grid to keep track of visited cells across all traversals. We'll iterate through each cell (r, c). If visited[r][c] is false, we start a new DFS traversal for the connected component of cells with the character grid[r][c].
The DFS function, say hasCycle(r, c, parent_r, parent_c), takes the current cell's coordinates and its parent's coordinates. This parent information is crucial to ensure we don't immediately travel back to the cell we just came from. Inside the DFS, we first mark the current cell as visited. Then, we explore its four neighbors. For any neighbor that has the same character, is not the parent, and has already been visited, we've found a cycle. If a neighbor is valid but unvisited, we recurse on it. If any recursive call finds a cycle, we propagate true up the call stack. If the entire grid is scanned and no cycle is found, we return false.
The problem states that a cycle must have a length of 4 or more. In a 2D grid where movement is restricted to four directions, the graph formed is bipartite. The smallest possible cycle in a bipartite graph has a length of 4 (e.g., a 2x2 square). Therefore, any cycle detected by this standard algorithm will automatically satisfy the length constraint.
class Solution { private int m, n; private char[][] grid; private boolean[][] visited; private int[] dr = {0, 0, 1, -1}; private int[] dc = {1, -1, 0, 0}; public boolean containsCycle(char[][] grid) { this.m = grid.length; this.n = grid[0].length; this.grid = grid; this.visited = new boolean[m][n]; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (!visited[i][j]) { if (dfs(i, j, -1, -1, grid[i][j])) { return true; } } } } return false; } private boolean dfs(int r, int c, int pr, int pc, char targetChar) { visited[r][c] = true; for (int i = 0; i < 4; i++) { int nr = r + dr[i]; int nc = c + dc[i]; if (nr >= 0 && nr < m && nc >= 0 && nc < n && grid[nr][nc] == targetChar) { if (nr == pr && nc == pc) { continue; // Don't go back to the parent immediately } if (visited[nr][nc]) { return true; // Found a cycle } if (dfs(nr, nc, r, c, targetChar)) { return true; } } } return false; }}Complexity
Time
O(m * n), where `m` is the number of rows and `n` is the number of columns. Each cell in the grid is visited at most once during the entire process.
Space
O(m * n). This space is used for the `visited` array (O(m * n)) and the recursion call stack. In the worst-case scenario of a path that traverses every cell, the stack depth can be up to O(m * n).
Trade-offs
Pros
The approach is intuitive and directly translates the problem into a standard graph traversal algorithm.
It is relatively easy to implement using recursion.
Cons
The recursive nature can lead to a
StackOverflowErroron very large grids with long, snake-like paths, as the recursion depth can become very large.The space complexity includes the recursion stack, which can be significant in the worst-case scenario.
Similar to the DFS approach, this method treats the grid as a graph and searches for cycles. However, it uses Breadth First Search (BFS), an iterative traversal algorithm, instead of recursion. By using a queue, BFS explores the graph layer by layer. This avoids the risk of stack overflow that can occur with a deep recursive DFS, making it a more robust solution for very large grids.
Algorithm
- Initialize a
visited[m][n]2D boolean array tofalse. - Iterate through each cell
(r, c)of the grid. - If
visited[r][c]isfalse, start a Breadth First Search (BFS) from this cell. - The BFS process for a component:
- Create a queue to store states. Each state will contain the cell's coordinates and its parent's coordinates, e.g.,
[row, col, parent_row, parent_col]. - Add the starting cell
[r, c, -1, -1]to the queue and markvisited[r][c]astrue. - While the queue is not empty:
a. Dequeue the current state
[curr_r, curr_c, parent_r, parent_c]. b. Explore the four neighbors(nr, nc)of(curr_r, curr_c). c. For each valid neighbor (in-bounds, same character): i. If the neighbor is the parent(parent_r, parent_c), ignore it. ii. Ifvisited[nr][nc]istrue, a cycle is found. Returntrue. iii. Otherwise, markvisited[nr][nc]astrueand enqueue the new state[nr, nc, curr_r, curr_c].
- Create a queue to store states. Each state will contain the cell's coordinates and its parent's coordinates, e.g.,
- If the main loop completes without any BFS finding a cycle, return
false.
Walkthrough
The core idea remains the same: traverse connected components of same-valued cells and check for back edges. We use a visited array to track visited cells. We iterate through the grid, and for any unvisited cell, we initiate a BFS. The BFS uses a queue to manage the cells to visit. To prevent trivial cycles, we store not just the cell's coordinates in the queue, but also its parent's coordinates (the cell from which it was discovered). When exploring neighbors of a cell, we skip the parent. If we encounter a neighbor that has the same character and has already been visited (and is not the parent), we have found a cycle and can immediately return true.
import java.util.Queue;import java.util.LinkedList; class Solution { public boolean containsCycle(char[][] grid) { int m = grid.length; int n = grid[0].length; boolean[][] visited = new boolean[m][n]; int[] dr = {0, 0, 1, -1}; int[] dc = {1, -1, 0, 0}; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (!visited[i][j]) { if (bfs(i, j, grid, visited, m, n, dr, dc)) { return true; } } } } return false; } private boolean bfs(int startR, int startC, char[][] grid, boolean[][] visited, int m, int n, int[] dr, int[] dc) { char targetChar = grid[startR][startC]; Queue<int[]> queue = new LinkedList<>(); queue.offer(new int[]{startR, startC, -1, -1}); // r, c, pr, pc visited[startR][startC] = true; while (!queue.isEmpty()) { int[] current = queue.poll(); int r = current[0]; int c = current[1]; int pr = current[2]; int pc = current[3]; for (int i = 0; i < 4; i++) { int nr = r + dr[i]; int nc = c + dc[i]; if (nr >= 0 && nr < m && nc >= 0 && nc < n && grid[nr][nc] == targetChar) { if (nr == pr && nc == pc) { continue; } if (visited[nr][nc]) { return true; // Cycle detected } visited[nr][nc] = true; queue.offer(new int[]{nr, nc, r, c}); } } } return false; }}Complexity
Time
O(m * n). Each cell is enqueued and processed at most once.
Space
O(m * n). This is for the `visited` array and the BFS queue. The queue can, in the worst case, hold a number of elements proportional to the total number of cells.
Trade-offs
Pros
Being an iterative approach, it is not susceptible to
StackOverflowError, making it more reliable for large inputs.BFS is guaranteed to find a shortest path (in terms of number of edges) from the source to any other node, which can be a useful property, although not strictly necessary for this problem.
Cons
The queue can grow to a large size, potentially up to O(m * n) elements for certain grid configurations, which might lead to high memory consumption.
A highly efficient approach for this problem is to use a Union-Find data structure, also known as a Disjoint Set Union (DSU). This data structure is optimized for tracking connectivity between elements. We treat each cell as an element in a set. We iterate through the grid, and for each cell, we check its neighbors (e.g., to the right and down). If two adjacent cells have the same character, we attempt to union their sets. If they are already in the same set (i.e., already connected by some other path), adding an edge between them forms a cycle.
Algorithm
- Create a Union-Find (Disjoint Set Union) data structure for
m * nelements. Each cell(r, c)is mapped to a unique integer index, e.g.,index = r * n + c. - Initialize the Union-Find structure such that each element is in its own set.
- Iterate through each cell
(r, c)of the grid. - For each cell, check its neighbors in two directions (e.g., right and down) to avoid processing each edge twice.
- Check right neighbor
(r, c+1):- If
c+1is within bounds andgrid[r][c] == grid[r][c+1]:- Find the representatives (roots) for the sets containing
(r, c)and(r, c+1). - If the representatives are the same, it means the two cells are already connected. Adding this edge creates a cycle. Return
true. - Otherwise, perform a
unionoperation on the two cells' sets.
- Find the representatives (roots) for the sets containing
- If
- Check down neighbor
(r+1, c):- If
r+1is within bounds andgrid[r][c] == grid[r+1][c]:- Find the representatives for
(r, c)and(r+1, c). - If they are the same, a cycle is detected. Return
true. - Otherwise,
uniontheir sets.
- Find the representatives for
- If
- If the loops complete without finding a cycle, return
false.
Walkthrough
We can flatten the 2D grid coordinates (r, c) into a 1D index r * n + c to use with a standard Union-Find implementation. The Union-Find data structure will maintain a parent array and optionally a rank or size array for optimization (union by rank/size and path compression).
We iterate through every cell (r, c). To avoid redundant checks, for each cell, we only consider forming edges with its right neighbor (r, c+1) and its down neighbor (r+1, c). When considering an edge between two cells, say u and v, we first use the find operation to check if they already belong to the same connected component. If find(u) == find(v), it implies there's already a path between them, and adding the direct edge (u, v) closes a loop, forming a cycle. In this case, we return true. If they are in different components, we merge them using the union operation and continue. If we process the entire grid without finding such a condition, no cycles exist.
class Solution { public boolean containsCycle(char[][] grid) { int m = grid.length; int n = grid[0].length; UnionFind uf = new UnionFind(m * n); for (int r = 0; r < m; r++) { for (int c = 0; c < n; c++) { int currentIndex = r * n + c; // Check down neighbor if (r + 1 < m && grid[r][c] == grid[r + 1][c]) { int downIndex = (r + 1) * n + c; if (uf.find(currentIndex) == uf.find(downIndex)) { return true; } uf.union(currentIndex, downIndex); } // Check right neighbor if (c + 1 < n && grid[r][c] == grid[r][c + 1]) { int rightIndex = r * n + (c + 1); if (uf.find(currentIndex) == uf.find(rightIndex)) { return true; } uf.union(currentIndex, rightIndex); } } } return false; }} class UnionFind { private int[] parent; private int[] rank; public UnionFind(int size) { parent = new int[size]; rank = new int[size]; for (int i = 0; i < size; i++) { parent[i] = i; rank[i] = 1; } } public int find(int i) { if (parent[i] == i) { return i; } return parent[i] = find(parent[i]); // Path compression } public void union(int i, int j) { int rootI = find(i); int rootJ = find(j); if (rootI != rootJ) { // Union by rank if (rank[rootI] > rank[rootJ]) { parent[rootJ] = rootI; } else if (rank[rootI] < rank[rootJ]) { parent[rootI] = rootJ; } else { parent[rootJ] = rootI; rank[rootI]++; } } }}Complexity
Time
O(m * n * α(m * n)), where `α` is the extremely slow-growing Inverse Ackermann function. For all practical purposes, the complexity is considered linear, O(m * n), as `α`'s value is less than 5 for any conceivable input size.
Space
O(m * n). This space is required for the `parent` and `rank` arrays of the Union-Find data structure.
Trade-offs
Pros
Extremely fast, with nearly constant time complexity per operation (amortized) due to path compression and union by rank/size optimizations.
It's an iterative approach, free from recursion depth limitations.
Often has lower constant factors than DFS/BFS, making it one of the most performant solutions in practice for cycle detection.
Cons
The underlying logic of why this detects cycles might be less intuitive than a direct graph traversal for those unfamiliar with the Union-Find data structure.
Requires implementing the Union-Find data structure, which is slightly more complex than a simple recursive DFS.
Solutions
Solution
class Solution {private int[] p;public boolean containsCycle(char[][] grid) { int m = grid.length; int n = grid[0].length; p = new int[m * n]; for (int i = 0; i < p.length; ++i) { p[i] = i; } int[] dirs = {0, 1, 0}; for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { for (int k = 0; k < 2; ++k) { int x = i + dirs[k]; int y = j + dirs[k + 1]; if (x < m && y < n && grid[i][j] == grid[x][y]) { if (find(x * n + y) == find(i * n + j)) { return true; } p[find(x * n + y)] = find(i * n + j); } } } } return false; }private int find(int x) { if (p[x] != x) { p[x] = find(p[x]); } return p[x]; }}/** * @param {character[][]} grid * @return {boolean} */ var containsCycle = function ( grid ) { const m = grid . length ; const n = grid [ 0 ]. length ; let p = Array . from ({ length : m * n }, ( _ , i ) => i ); function find ( x ) { if ( p [ x ] != x ) { p [ x ] = find ( p [ x ]); } return p [ x ]; } const dirs = [ 0 , 1 , 0 ]; for ( let i = 0 ; i < m ; ++ i ) { for ( let j = 0 ; j < n ; ++ j ) { for ( let k = 0 ; k < 2 ; ++ k ) { const x = i + dirs [ k ]; const y = j + dirs [ k + 1 ]; if ( x < m && y < n && grid [ x ][ y ] == grid [ i ][ j ]) { if ( find ( x * n + y ) == find ( i * n + j )) { return true ; } p [ find ( x * n + y )] = find ( i * n + j ); } } } } return false ; };class Solution {public: vector<int> p; bool containsCycle(vector<vector<char>> &grid) { int m = grid.size(), n = grid[0].size(); p.resize(m * n); for (int i = 0; i < p.size(); ++i) p[i] = i; vector<int> dirs = {0, 1, 0}; for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { for (int k = 0; k < 2; ++k) { int x = i + dirs[k], y = j + dirs[k + 1]; if (x < m && y < n && grid[x][y] == grid[i][j]) { if (find(x * n + y) == find(i * n + j)) return 1; p[find(x * n + y)] = find(i * n + j); } } } } return 0; } int find(int x) { if (p[x] != x) p[x] = find(p[x]); return p[x]; }};class Solution: def containsCycle(self, grid: List[List[str]]) -> bool: def find(x): if p[x] != x: p[x] = find(p[x]) return p[x] m, n = len(grid), len(grid[0]) p = list(range(m * n)) for i in range(m): for j in range(n): for a, b in [[0, 1], [1, 0]]: x, y = i + a, j + b if x < m and y < n and grid[x][y] == grid[i][j]: if find(x * n + y) == find(i * n + j): return True p[find(x * n + y)] = find(i * n + j) return FalseVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.