5 Best Ways to Detect Cycles in a 2D Grid in Python

πŸ’‘ Problem Formulation: Detecting cycles in a 2D grid involves determining if there is a closed path such that one can travel from a cell back to itself without revisiting any cell. This problem has applications in various domains, such as pathfinding in robotics, puzzle solving, and graph theory. An example input might be a grid represented as a 2D array, with certain cells marked as barriers. The desired output is a boolean indication of whether or not there is a cycle in the grid.

Method 1: Depth-First Search (DFS)

This method uses the DFS algorithm to explore the grid by moving to adjacent cells, marking visited cells, and backtracking if a dead end is reached. It’s useful for detecting cycles by checking whether an adjacent cell visited is part of the current path, indicating a cycle. The function will take a 2D grid as input and return a boolean value indicating the presence of a cycle.

Here’s an example:

def isCyclic(grid, x, y, visited, parent_x, parent_y):
    rows, cols = len(grid), len(grid[0])
    visited[x][y] = True
    direction = [(0, 1), (1, 0), (0, -1), (-1, 0)]

    for dx, dy in direction:
        new_x, new_y = x + dx, y + dy
        if 0 <= new_x < rows and 0 <= new_y < cols and not grid[new_x][new_y]:
            if not visited[new_x][new_y]:
                if isCyclic(grid, new_x, new_y, visited, x, y):
                    return True
            elif parent_x != new_x or parent_y != new_y:
                return True
    return False

# grid without a cycle
grid = [[0, 0], [0, 0]]
visited = [[False for _ in range(len(grid[0]))] for _ in range(len(grid))]
print(isCyclic(grid, 0, 0, visited, -1, -1))

Output:

False

This code snippet defines the function isCyclic() which implements DFS to detect cycles in a 2D grid. It recursively explores surrounding cells, marking the ones that have been visited. If an already visited cell is encountered again, there is a cycle. In this example, the grid provided does not contain any cycle, and the function correctly returns False.

Method 2: Union-Find Algorithm

The Union-Find algorithm is a data structure that keeps track of elements divided into one or more disjoint sets. It’s useful for cycle detection in a grid as it helps to identify if adding an edge (adjacent cells) would form a cycle by checking if they belong to the same set. The function will take pairwise cell connections as input and manage sets to detect cycles.

Here’s an example:

class UnionFind:
    def __init__(self, grid):
        self.parent = {}
        for row in range(len(grid)):
            for col in range(len(grid[0])):
                if grid[row][col] == 0:
                    self.parent[(row, col)] = (row, col)

    def find(self, node):
        if self.parent[node] != node:
            self.parent[node] = self.find(self.parent[node])
        return self.parent[node]

    def union(self, node1, node2):
        root1, root2 = self.find(node1), self.find(node2)
        if root1 != root2:
            self.parent[root2] = root1
            return False
        return True

def hasCycle(grid):
    uf = UnionFind(grid)
    direction = [(0, 1), (1, 0)]
    for row in range(len(grid)):
        for col in range(len(grid[0])):
            if grid[row][col] == 0:
                node1 = (row, col)
                for dx, dy in direction:
                    node2 = (row + dx, col + dy)
                    if node2 in uf.parent and uf.union(node1, node2):
                        return True
    return False

# grid with a cycle
cycle_grid = [[0, 0], [0, 0]]
print(hasCycle(cycle_grid))

Output:

True

The UnionFind class uses path compression and union-by-rank to efficiently manage disjoint sets. The function hasCycle() uses the Union-Find data structure to detect cycles. It iterates over the grid and applies union operations. If ever it connects two cells that are already in the same set, a cycle is detected, and it returns True. For the provided cycle_grid, which includes a cycle, the function accurately returns True.

Method 3: Breadth-First Search (BFS)

Breadth-First Search (BFS) traverses the grid level by level, starting from a given cell and visiting all its adjacent cells. Cycle detection with BFS involves checking if an adjacent cell has been visited before while ensuring it’s not the immediate predecessor. This method is well-suited for grids where shortest-cycle detection is a priority. The function will output a boolean value indicating the presence of a cycle.

Here’s an example:

from collections import deque

def bfs_isCyclic(grid):
    visited = [[False]*len(row) for row in grid]
    direction = [(0, 1), (1, 0), (0, -1), (-1, 0)]
    queue = deque([(0, 0, -1, -1)])
    
    while queue:
        x, y, from_x, from_y = queue.popleft()
        if visited[x][y]:
            continue
        
        visited[x][y] = True
        for dx, dy in direction:
            next_x, next_y = x + dx, y + dy
            if 0 <= next_x < len(grid) and 0 <= next_y < len(grid[0]) and not grid[next_x][next_y]:
                if visited[next_x][next_y] and (next_x != from_x or next_y != from_y):
                    return True
                queue.append((next_x, next_y, x, y))
    return False

# grid without a cycle
no_cycle_grid = [[0, 0, 0], [0, 1, 0], [0, 0, 0]]
print(bfs_isCyclic(no_cycle_grid))

Output:

False

This code snippet defines a function bfs_isCyclic() which uses BFS to search for cycles in a 2D grid. A queue is used to store and process cells by levels. Cells are marked as visited and their adjacent cells are checked. If a visited cell that is not the parent is found, it indicates a cycle. The no_cycle_grid variable holds a grid without a cycle, and the function ultimately returns False.

Method 4: Recursive Backtracking

Recursive Backtracking similar to DFS, but with a more explicit backtracking mechanism, explores all possible paths in a grid while maintaining a history of the path. If a cell is encountered that is already part of the path, a cycle is detected. This method is intuitive and straightforward to implement. It returns a boolean value to confirm the presence of a cycle in the grid.

Here’s an example:

def backtrack_isCyclic(grid, x, y, visited, path):
    if visited[x][y] and (x, y) in path:
        return True
    visited[x][y] = True
    path.add((x, y))

    direction = [(0, 1), (1, 0), (0, -1), (-1, 0)]
    for dx, dy in direction:
        next_x, next_y = x + dx, y + dy
        if 0 <= next_x < len(grid) and 0 <= next_y < len(grid[0]) and not grid[next_x][next_y]:
            if not visited[next_x][next_y]:
                if backtrack_isCyclic(grid, next_x, next_y, visited, path):
                    return True
            elif (next_x, next_y) in path and (next_x, next_y) != (x, y):
                return True

    path.remove((x, y))
    return False

# grid without a cycle
another_no_cycle_grid = [[0, 0, 0], [0, 1, 0], [0, 0, 0]]
visited = [[False]*len(row) for row in another_no_cycle_grid]
path = set()
print(backtrack_isCyclic(another_no_cycle_grid, 0, 0, visited, path))

Output:

False

In the backtrack_isCyclic() function, we initialize a path set and visited array to keep track of visited cells and the current exploration path. The function recursively explores all adjacent cells, backtracks when a dead-end is encountered, and checks for cycles by examining if a cell is revisited. Since the grid another_no_cycle_grid does not have a cycle, the function correctly returns False.

Bonus One-Liner Method 5: NetworkX Library

NetworkX is a Python library for studying graphs and networks. It provides built-in functions for cycle detection in graph structures, which can be applied to 2D grids after conversion. Using NetworkX drastically simplifies the code required to detect cycles by leveraging its powerful graph analysis algorithms.

Here’s an example:

import networkx as nx

def has_cycle_with_networkx(grid):
    G = nx.Graph()
    for x in range(len(grid)):
        for y in range(len(grid[0])):
            if grid[x][y] == 0:
                G.add_node((x, y))
                if x > 0 and grid[x-1][y] == 0:
                    G.add_edge((x-1, y), (x, y))
                if y > 0 and grid[x][y-1] == 0:
                    G.add_edge((x, y-1), (x, y))
    return any(nx.simple_cycles(G))

# grid with a cycle
sample_grid_with_cycle = [[0, 0], [0, 0]]
print(has_cycle_with_networkx(sample_grid_with_cycle))

Output:

True

The function has_cycle_with_networkx() converts the grid into a graph where each cell is a node and edges represent adjacent non-barrier cells. The NetworkX function simple_cycles() is then used to detect cycles in this graph. In the given sample_grid_with_cycle, which contains a cycle, the function uses NetworkX to confirm this by returning True.

Summary/Discussion

  • Method 1: Depth-First Search (DFS). Strengths: Conducive to recursive solutions and can be memory efficient. Weaknesses: Might be slow for large grids and not necessarily detect the shortest cycle.
  • Method 2: Union-Find Algorithm. Strengths: Very efficient for disjoint set operations and scales well. Weaknesses: A bit more complex to understand and implement than other methods.
  • Method 3: Breadth-First Search (BFS). Strengths: Can quickly find the shortest cycle and works well for broader grids. Weaknesses: Can consume more memory, and is not as simple for recursive implementations.
  • Method 4: Recursive Backtracking. Strengths: Simple to understand and implement. Weak for large grids: Can lead to a stack overflow due to deep recursion.
  • Method 5: NetworkX Library. Strengths: Very simple to implement with powerful graph algorithms. Weaknesses: Requires an additional library and the overhead of converting a grid to a graph.