LeetCode 2510 - Check if There is a Path With Equal Number of 0's And 1's

This problem asks us to determine if there exists a path in a binary matrix from the top-left corner (0, 0) to the bottom-right corner (m - 1, n - 1) such that the number of 0s visited along the path is equal to the number of 1s.

LeetCode Problem 2510

Difficulty: 🟡 Medium
Topics: Array, Dynamic Programming, Matrix

Solution

Problem Understanding

This problem asks us to determine if there exists a path in a binary matrix from the top-left corner (0, 0) to the bottom-right corner (m - 1, n - 1) such that the number of 0s visited along the path is equal to the number of 1s. The matrix is binary, so each cell contains either 0 or 1. Movement is restricted to either down or right, which defines the possible paths.

The input is a 2D list grid of dimensions m x n, where 2 <= m, n <= 100. The output is a boolean value: True if a valid path exists, False otherwise.

Important considerations include:

  1. Paths cannot move diagonally or backward, limiting the search space but still potentially exponential in size for brute-force solutions.
  2. The matrix size is moderate (up to 100 x 100), which discourages naive DFS that explores all paths without pruning.
  3. Since we are counting 0s and 1s, the difference between the counts along a path is an important state to track.

Edge cases that can trip up naive implementations include grids where the total number of cells is odd (impossible to split equally between 0s and 1s), or grids where movement is constrained in such a way that certain counts are impossible to achieve.

Approaches

Brute Force Approach

The brute-force method explores all possible paths from (0, 0) to (m - 1, n - 1) recursively. At each step, we track the number of 0s and 1s visited so far. When we reach the destination, we check if the counts are equal. While this guarantees correctness, it has exponential time complexity O(2^(m+n)) because each cell can branch into two directions. It also requires maintaining state for each path, leading to high memory usage.

Optimal Approach

The key insight is that the problem can be transformed into a dynamic programming problem where the state is (row, col, diff), with diff = count_of_ones - count_of_zeros. At each cell, we only need to track which differences have been achieved along any path to that cell. By propagating these differences using a set per cell, we can efficiently avoid exploring redundant paths with the same diff. This reduces the search space significantly because diff is bounded by -(m*n) to m*n, but practically only feasible differences matter as we move down or right.

Comparison of approaches:

Approach Time Complexity Space Complexity Notes
Brute Force O(2^(m+n)) O(m+n) recursion depth Explores all paths recursively; too slow for m, n up to 100
Optimal O(m * n * min(m*n, total_diff_range)) O(m * n * possible_diffs) Uses dynamic programming with sets to track differences

Algorithm Walkthrough

  1. Initialize a 2D array dp of size m x n where each element is a set that stores the possible diff values at that cell.
  2. At (0, 0), initialize diff = 1 if grid[0][0] == 1 else -1.
  3. Iterate through each cell (i, j) in the matrix row by row:
  • For each possible diff in dp[i-1][j] (from top) or dp[i][j-1] (from left), compute the new diff by adding 1 if grid[i][j] == 1 or -1 if 0.
  • Add the new diff to dp[i][j].
  1. After filling the DP table, check if 0 exists in dp[m-1][n-1]. If yes, there is a path with equal number of 0s and 1s.
  2. Return True if 0 exists, False otherwise.

Why it works: By propagating all feasible differences (count_ones - count_zeros) through the DP table, we ensure that we consider all possible paths without revisiting identical states. The invariant is that dp[i][j] always contains every difference achievable by some path to that cell.

Python Solution

from typing import List

class Solution:
    def isThereAPath(self, grid: List[List[int]]) -> bool:
        m, n = len(grid), len(grid[0])
        dp = [[set() for _ in range(n)] for _ in range(m)]
        dp[0][0].add(1 if grid[0][0] == 1 else -1)
        
        for i in range(m):
            for j in range(n):
                if i == 0 and j == 0:
                    continue
                current_set = set()
                if i > 0:
                    for diff in dp[i-1][j]:
                        current_set.add(diff + (1 if grid[i][j] == 1 else -1))
                if j > 0:
                    for diff in dp[i][j-1]:
                        current_set.add(diff + (1 if grid[i][j] == 1 else -1))
                dp[i][j] = current_set
        
        return 0 in dp[m-1][n-1]

Explanation: The code initializes a DP table of sets to store differences at each cell. We iterate through the matrix and propagate differences from top and left neighbors, updating the current cell's set. Finally, we check if 0 is reachable at the destination.

Go Solution

func isThereAPath(grid [][]int) bool {
    m, n := len(grid), len(grid[0])
    dp := make([][]map[int]struct{}, m)
    for i := 0; i < m; i++ {
        dp[i] = make([]map[int]struct{}, n)
        for j := 0; j < n; j++ {
            dp[i][j] = make(map[int]struct{})
        }
    }

    startDiff := -1
    if grid[0][0] == 1 {
        startDiff = 1
    }
    dp[0][0][startDiff] = struct{}{}

    for i := 0; i < m; i++ {
        for j := 0; j < n; j++ {
            if i == 0 && j == 0 {
                continue
            }
            currSet := make(map[int]struct{})
            if i > 0 {
                for diff := range dp[i-1][j] {
                    newDiff := diff
                    if grid[i][j] == 1 {
                        newDiff += 1
                    } else {
                        newDiff -= 1
                    }
                    currSet[newDiff] = struct{}{}
                }
            }
            if j > 0 {
                for diff := range dp[i][j-1] {
                    newDiff := diff
                    if grid[i][j] == 1 {
                        newDiff += 1
                    } else {
                        newDiff -= 1
                    }
                    currSet[newDiff] = struct{}{}
                }
            }
            dp[i][j] = currSet
        }
    }

    _, exists := dp[m-1][n-1][0]
    return exists
}

Go-specific notes: We use map[int]struct{} to mimic a set, since Go does not have a native set type. Struct{} is used as a memory-efficient placeholder.

Worked Examples

Example 1: grid = [[0,1,0,0],[0,1,0,0],[1,0,1,0]]

Step Cell Propagated diff set
Start (0,0) {-1}
(0,1) right {-1 + 1} = {0}
(1,0) down {-1 + -1} = {-2}
... ... ...
End (2,3) {0, others} → contains 0 → True

Example 2: grid = [[1,1,0],[0,0,1],[1,0,0]]

DP propagates differences, but (2,2) does not contain 0 → False

Complexity Analysis

Measure Complexity Explanation
Time O(m * n * D) D is the maximum range of achievable differences at a cell, bounded by m*n
Space O(m * n * D) Each cell stores a set of differences

The dynamic programming approach reduces the exponential search space to a polynomial-time algorithm by tracking only achievable differences.

Test Cases

sol = Solution()

# Provided examples
assert sol.isThereAPath([[0,1,0,0],[0,1,0,0],[1,0,1,0]]) == True  # path exists
assert sol.isThereAPath([[1,1,0],[0,0,1],[1,0,0]]) == False  # no path

# Edge cases
assert sol.isThereAPath([[0,1],[1,0]]) == True  # minimal grid with valid path
assert sol.isThereAPath([[0,0],[0,0]]) == False  # all zeros, impossible
assert sol.isThereAPath([[1,