5 Best Ways to Find Maximum Path Length in a Binary Matrix in Python

Rate this post

πŸ’‘ Problem Formulation: The task is to determine the longest path length in a binary matrix, where the path can only progress from ‘1’ to ‘1’ horizontally or vertically. Given a binary matrix, which is a 2D list with elements consisting of 0’s and 1’s, our goal is to find the maximum length of a contiguous path of 1’s. For example, in the matrix [[1,1,0],[0,1,1],[1,0,0]], the maximum path length is 3.

Method 1: Depth-First Search (DFS) Recursive Algorithm

The Depth-First Search (DFS) approach exhaustively explores each possible path of 1’s starting from each cell, marking visited paths along the way to avoid cycles. The function iteratively moves up, down, left, or right and adds to a current path length, returning the maximum path length found.

Here’s an example:

def max_path_length_dfs(matrix):
    def dfs(i, j):
        if not (0 <= i < len(matrix) and 0 <= j < len(matrix[0]) and matrix[i][j]):
            return 0
        matrix[i][j] = 0  # mark as visited
        length = 1 + max(dfs(i+1, j), dfs(i-1, j), dfs(i, j+1), dfs(i, j-1))
        matrix[i][j] = 1  # unmark for other paths
        return length
    
    max_length = 0
    for i in range(len(matrix)):
        for j in range(len(matrix[i])):
            if matrix[i][j]:
                max_length = max(max_length, dfs(i, j))
    
    return max_length

# Example usage:
binary_matrix = [[1,1,0],[0,1,1],[1,0,0]]
print(max_path_length_dfs(binary_matrix))

Output:

3

This code snippet defines a function max_path_length_dfs() that performs a depth-first search starting from each ‘1’ in the matrix and keeps track of the longest path length found. After exploring each contiguous path, the cell is unmarked to allow other paths to use it. The main loop iterates over each cell, performing DFS where a ‘1’ is found and updating the max_length variable accordingly.

Method 2: Breadth-First Search (BFS) Iterative Algorithm

The Breadth-First Search (BFS) algorithm uses a queue to iteratively explore paths from each initial ‘1’. It typically finds the shortest path in unweighted graphs, but can be adapted to compute the longest path in a binary matrix.

Here’s an example:

from collections import deque

def max_path_length_bfs(matrix):
    def bfs(i, j):
        queue = deque([(i, j, 1)])  # include initial path length
        max_len = 1
        matrix[i][j] = 0  # mark as visited

        while queue:
            x, y, length = queue.popleft()
            for dx, dy in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
                nx, ny = x + dx, y + dy
                if 0 <= nx < len(matrix) and 0 <= ny < len(matrix[0]) and matrix[nx][ny]:
                    queue.append((nx, ny, length + 1))
                    matrix[nx][ny] = 0  # mark as visited
                    max_len = max(max_len, length + 1)
        
        # Reset visited to '1'
        queue = deque([(i, j)])
        while queue:
            x, y = queue.popleft()
            for dx, dy in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
                nx, ny = x + dx, y + dy
                if 0 <= nx < len(matrix) and 0 <= ny < len(matrix[0]) and matrix[nx][ny] == 0:
                    queue.append((nx, ny))
                    matrix[nx][ny] = 1
        return max_len
    
    max_length = 0
    for i in range(len(matrix)):
        for j in range(len(matrix[0])):
            if matrix[i][j]:
                max_length = max(max_length, bfs(i, j))

    return max_length

# Example usage:
binary_matrix = [[1,1,0],[0,1,1],[1,0,0]]
print(max_path_length_bfs(binary_matrix))

Output:

3

This code demonstrates a max_path_length_bfs() function that uses a breadth-first search (BFS) approach. It stores the coordinates with the path length in a queue, iterating over each node’s neighbors. After exploring each path, it resets the visited cells back to ‘1’, allowing for other paths exploration. This approach is also layer-wise, meaning that it explores all nodes at a particular distance before moving on.

Method 3: Dynamic Programming

Dynamic Programming (DP) is a method for solving complex problems by breaking them down into simpler subproblems. It is typically used for optimization problems, where it can capitalize on overlapping subproblems and thus prevent redundant computations. In the context of a binary matrix, DP can track the maximum path length ending at each cell.

Here’s an example:

def max_path_length_dp(matrix):
    if not matrix or not matrix[0]: return 0
    
    nrows, ncols = len(matrix), len(matrix[0])
    max_length = 0
    dp = [[0]*ncols for _ in range(nrows)]

    for i in range(nrows):
        for j in range(ncols):
            if matrix[i][j]:
                dp[i][j] = 1
                if i > 0: dp[i][j] = max(dp[i][j], dp[i-1][j] + 1)
                if j > 0: dp[i][j] = max(dp[i][j], dp[i][j-1] + 1)
                max_length = max(max_length, dp[i][j])
    
    return max_length

# Example usage:
binary_matrix = [[1,1,0],[0,1,1],[1,0,0]]
print(max_path_length_dp(binary_matrix))

Output:

3

The max_path_length_dp() function uses a dynamic programming table dp to store the maximum path length that can be reached up to each cell (i, j) in the matrix. The value of dp[i][j] is obtained by looking at the immediately left (j-1) and above (i-1) cells if they are part of the path (have a value of ‘1’). The overall maximum value in dp is the length of the longest path in the binary matrix.

Method 4: Union-Find Data Structure

The Union-Find data structure, also known as the disjoint set, can be used to keep track of elements split into one or more disjoint sets. It is an efficient algorithm for both connecting elements and finding which set an element belongs to. By finding the largest set of connected ‘1’s, we can use the Union-Find data structure to find the maximum path length.

Here’s an example:

class UnionFind:
    def __init__(self, size):
        self.parent = list(range(size))
        self.rank = [0] * size
        self.size = [1] * size

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

    def union(self, x, y):
        rootX = self.find(x)
        rootY = self.find(y)
        if rootX != rootY:
            if self.rank[rootX] > self.rank[rootY]:
                self.parent[rootY] = rootX
                self.size[rootX] += self.size[rootY]
            elif self.rank[rootX] < self.rank[rootY]:
                self.parent[rootX] = rootY
                self.size[rootY] += self.size[rootX]
            else:
                self.parent[rootY] = rootX
                self.size[rootX] += self.size[rootY]
                self.rank[rootX] += 1

def max_path_length_union_find(matrix):
    nrows, ncols = len(matrix), len(matrix[0])
    uf = UnionFind(nrows * ncols)

    # Convert 2D positions to 1D and perform unions
    def get_id(x, y): return x * ncols + y

    for r in range(nrows):
        for c in range(ncols):
            if matrix[r][c] == 1:
                if r + 1 < nrows and matrix[r+1][c] == 1:
                    uf.union(get_id(r, c), get_id(r+1, c))
                if c + 1 < ncols and matrix[r][c+1] == 1:
                    uf.union(get_id(r, c), get_id(r, c+1))
    
    return max(uf.size)

# Example usage:
binary_matrix = [[1,1,0],[0,1,1],[1,0,0]]
print(max_path_length_union_find(binary_matrix))

Output:

3

The code uses a Union-Find data structure provided by the UnionFind class with methods find() and union(). The max_path_length_union_find() function converts the 2D binary matrix positions into 1D to track the size of sets representing connected ‘1’s. When two adjacent ‘1’s are found, the sets they belong to are merged using a union operation. The size of the largest set is then the maximum path length.

Bonus One-Liner Method 5: Using External Libraries

Python’s extensive ecosystem includes libraries such as NumPy and SciPy that can simplify many tasks. For the problem at hand, these libraries may have specialized functions or data structures that can be adapted for use. Here we show a way to leverage these libraries to find the maximum path length using connected components.

Here’s an example:

import numpy as np
from scipy.ndimage import label

def max_path_length_scipy(matrix):
    labeled_array, num_features = label(matrix)
    return max(np.sum(labeled_array == i) for i in range(1, num_features + 1))

# Example usage:
binary_matrix = np.array([[1,1,0],[0,1,1],[1,0,0]])
print(max_path_length_scipy(binary_matrix))

Output:

3

In this code snippet, the max_path_length_scipy() function makes use of the label() function from the SciPy library, which labels connected components in the binary matrix. It returns the largest sum of elements in any label, effectively giving the maximum path length.

Summary/Discussion

  • Method 1: Depth-First Search (DFS). Powerful for many graph-related problems. Computationally expensive and can be slow on large matrices or intricate connections.
  • Method 2: Breadth-First Search (BFS). Iterative and level-based. Can be more memory intensive but gives a systematic approach to node exploration.
  • Method 3: Dynamic Programming (DP). Eliminates redundant computations and can be efficient for large inputs, but requires careful formulation of the problem.
  • Method 4: Union-Find. Very efficient for problems involving connectivity in graphs. The complexity can be harder to grasp and implement for beginners.
  • Bonus Method 5: Using External Libraries. Can offer succinct and optimized solutions. However, they add external dependencies and may obfuscate the underlying process.