Facebook Pixel

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

Problem Description

You have a 2D binary grid (containing only 0s and 1s) with m rows and n columns. Starting from the top-left corner at position (0, 0), you need to reach the bottom-right corner at position (m-1, n-1).

From any cell (row, col), you can only move in two directions:

  • Down: to cell (row + 1, col)
  • Right: to cell (row, col + 1)

Your task is to determine if there exists a path from (0, 0) to (m-1, n-1) where you visit an equal number of 0s and 1s along the way.

Return true if such a path exists, otherwise return false.

For example, if you have a 3×3 grid and your path visits cells containing values [1, 0, 1, 1, 0], you've visited three 1s and two 0s, which are not equal, so this wouldn't be a valid path. You need to find if any possible path from start to end has exactly the same count of 0s and 1s.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that any path from (0, 0) to (m-1, n-1) will have exactly m + n - 1 cells visited (including start and end positions). This is because we need to make m-1 down moves and n-1 right moves to reach the destination.

For the path to have an equal number of 0s and 1s, we need (m + n - 1) / 2 zeros and (m + n - 1) / 2 ones. This immediately tells us that if m + n - 1 is odd, it's impossible to have equal counts, so we can return false right away.

When m + n - 1 is even, we need to track how many 1s we've collected along our path. We can use dynamic programming with memoization to explore all possible paths. The state we need to track is:

  • Current position (i, j)
  • Number of 1s collected so far k

We can prune our search early using two observations:

  1. If we've already collected more 1s than the target count s = (m + n - 1) / 2, we can stop exploring this path
  2. If the remaining cells aren't enough to collect the required number of 0s, we can also stop. The number of 0s collected so far is (i + j + 1 - k), and if this exceeds our target s, we know this path won't work

At the destination (m-1, n-1), we check if we've collected exactly s ones (which means we also collected exactly s zeros), making the counts equal.

Learn more about Dynamic Programming patterns.

Solution Approach

The solution uses memoization search (dynamic programming with recursion and caching) to explore all possible paths from the top-left to bottom-right corner.

First, we check if a valid path is even possible:

  • Calculate the total path length: s = m + n - 1
  • If s is odd (checked using s & 1), return false immediately since we can't split an odd number equally
  • If even, set s = s / 2 (using s >>= 1 for efficiency) as our target count for both 0s and 1s

The main algorithm uses a DFS function dfs(i, j, k) where:

  • i, j represents the current position in the grid
  • k tracks the number of 1s collected so far

For each recursive call:

  1. Boundary check: If we go out of bounds (i >= m or j >= n), return false

  2. Update count: Add the current cell's value to our 1s count: k += grid[i][j]

  3. Pruning conditions:

    • If k > s: We've collected too many 1s, impossible to balance
    • If i + j + 1 - k > s: We've collected too many 0s (since zeros collected = cells visited - ones collected), impossible to balance
  4. Base case: If we reach the destination (i == m-1 and j == n-1), check if k == s (exactly half 1s means exactly half 0s too)

  5. Recursive exploration: Try both possible moves:

    • Move down: dfs(i + 1, j, k)
    • Move right: dfs(i, j + 1, k)
    • Return true if either path succeeds

The @cache decorator memoizes the results, preventing redundant calculations for the same state (i, j, k), which significantly improves performance from exponential to polynomial time complexity.

The algorithm starts with dfs(0, 0, 0) from the top-left corner with zero 1s collected initially.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example with a 3×3 grid:

Grid:
[1, 0, 1]
[0, 1, 0]  
[1, 0, 1]

Step 1: Check if equal counts are possible

  • Path length = m + n - 1 = 3 + 3 - 1 = 5 cells
  • Since 5 is odd, we cannot have equal 0s and 1s (can't split 5 equally)
  • Return false immediately

Let's try another example with a 2×3 grid:

Grid:
[1, 0, 1]
[0, 1, 1]

Step 1: Check path length

  • Path length = 2 + 3 - 1 = 4 cells
  • Since 4 is even, we can potentially have 2 ones and 2 zeros
  • Target count s = 4 / 2 = 2

Step 2: Start DFS from (0,0)

  • Call dfs(0, 0, 0) - starting at top-left with 0 ones collected

Step 3: Process cell (0,0)

  • Current cell value = 1, so k = 0 + 1 = 1
  • Check pruning: k=1 ≤ s=2 ✓ and zeros=1 ≤ s=2 ✓
  • Not at destination, explore both moves

Step 4: Explore from (0,0)

Path 1: Go right to (0,1)

  • dfs(0, 1, 1) - carrying 1 one
  • Cell value = 0, so k stays 1
  • Cells visited = 2, zeros = 2-1 = 1 ≤ 2 ✓
  • From (0,1), can go right to (0,2) or down to (1,1)

Path 1a: (0,0)→(0,1)→(0,2)→(1,2)

  • At (0,2): k = 1+1 = 2, zeros = 1
  • At (1,2): k = 2+1 = 3, zeros = 1
  • k > s, prune this path ✗

Path 1b: (0,0)→(0,1)→(1,1)→(1,2)

  • At (1,1): k = 1+1 = 2, zeros = 1
  • At (1,2): k = 2+1 = 3, zeros = 1
  • k > s, prune this path ✗

Path 2: Go down to (1,0)

  • dfs(1, 0, 1) - carrying 1 one
  • Cell value = 0, so k stays 1
  • From (1,0), must go right (only option to reach destination)

Path 2a: (0,0)→(1,0)→(1,1)→(1,2)

  • At (1,1): k = 1+1 = 2, zeros = 2
  • At (1,2): k = 2+1 = 3, zeros = 2
  • k > s, prune this path ✗

Step 5: Check all paths All paths result in either:

  • 3 ones and 1 zero (paths through multiple 1s)
  • 2 ones and 2 zeros (if we could find such a path)

Let's trace one valid scenario with a modified grid:

Grid:
[1, 0, 0]
[0, 1, 1]

Path: (0,0)→(0,1)→(0,2)→(1,2)

  • Values visited: [1, 0, 0, 1]
  • Count: 2 ones, 2 zeros ✓
  • This would return true

The memoization ensures that if we encounter the same state (i, j, k) again, we reuse the cached result instead of recalculating, making the algorithm efficient.

Solution Implementation

1from typing import List
2from functools import cache
3
4class Solution:
5    def isThereAPath(self, grid: List[List[int]]) -> bool:
6        """
7        Determines if there exists a path from top-left to bottom-right
8        with equal number of 0s and 1s.
9      
10        Args:
11            grid: 2D binary grid containing only 0s and 1s
12          
13        Returns:
14            True if such a path exists, False otherwise
15        """
16      
17        @cache
18        def dfs(row: int, col: int, ones_count: int) -> bool:
19            """
20            Recursive DFS to explore paths and track count of 1s.
21          
22            Args:
23                row: Current row position
24                col: Current column position
25                ones_count: Number of 1s encountered so far in the path
26              
27            Returns:
28                True if a valid path exists from current position
29            """
30            # Check if out of bounds
31            if row >= rows or col >= cols:
32                return False
33          
34            # Update count of 1s with current cell value
35            ones_count += grid[row][col]
36          
37            # Pruning: if we have too many 1s or too many 0s already
38            # Early termination when it's impossible to reach target
39            zeros_count = row + col + 1 - ones_count  # Total cells visited minus ones
40            if ones_count > target_ones or zeros_count > target_ones:
41                return False
42          
43            # Check if we reached destination with correct count
44            if row == rows - 1 and col == cols - 1:
45                return ones_count == target_ones
46          
47            # Try moving down or right
48            return dfs(row + 1, col, ones_count) or dfs(row, col + 1, ones_count)
49      
50        # Initialize grid dimensions
51        rows, cols = len(grid), len(grid[0])
52      
53        # Calculate total path length (number of cells in any path)
54        path_length = rows + cols - 1
55      
56        # If path length is odd, we can't have equal 0s and 1s
57        if path_length & 1:  # Bitwise AND to check if odd
58            return False
59      
60        # Target number of 1s should be half of path length
61        target_ones = path_length >> 1  # Bitwise right shift (divide by 2)
62      
63        # Start DFS from top-left corner
64        return dfs(0, 0, 0)
65
1class Solution {
2    private int targetOnesCount;
3    private int rows;
4    private int cols;
5    private int[][] grid;
6    private Boolean[][][] memo;
7
8    public boolean isThereAPath(int[][] grid) {
9        // Initialize grid dimensions
10        rows = grid.length;
11        cols = grid[0].length;
12        this.grid = grid;
13      
14        // Total cells in any path from top-left to bottom-right
15        int pathLength = rows + cols - 1;
16      
17        // Memoization array: memo[row][col][onesCount] stores whether a valid path exists
18        // from (row, col) to destination with exactly 'onesCount' ones so far
19        memo = new Boolean[rows][cols][pathLength];
20      
21        // For equal 0s and 1s, path length must be even
22        if (pathLength % 2 == 1) {
23            return false;
24        }
25      
26        // Target number of ones (half of total path length for equal distribution)
27        targetOnesCount = pathLength / 2;
28      
29        // Start DFS from top-left corner with 0 ones counted
30        return dfs(0, 0, 0);
31    }
32
33    private boolean dfs(int row, int col, int onesCount) {
34        // Check if current position is out of bounds
35        if (row >= rows || col >= cols) {
36            return false;
37        }
38      
39        // Update ones count based on current cell value
40        onesCount += grid[row][col];
41      
42        // Return memoized result if already computed
43        if (memo[row][col][onesCount] != null) {
44            return memo[row][col][onesCount];
45        }
46      
47        // Pruning: Check if it's impossible to achieve target
48        // 1. If current ones count exceeds target
49        // 2. If remaining cells can't provide enough zeros (cells traversed - onesCount > target zeros)
50        int cellsTraversed = row + col + 1;
51        int zerosCount = cellsTraversed - onesCount;
52        if (onesCount > targetOnesCount || zerosCount > targetOnesCount) {
53            return false;
54        }
55      
56        // Check if we reached the destination
57        if (row == rows - 1 && col == cols - 1) {
58            // Valid path only if we have exactly the target number of ones
59            return onesCount == targetOnesCount;
60        }
61      
62        // Try moving down or right
63        boolean canReachDestination = dfs(row + 1, col, onesCount) || dfs(row, col + 1, onesCount);
64      
65        // Memoize and return the result
66        memo[row][col][onesCount] = canReachDestination;
67        return canReachDestination;
68    }
69}
70
1class Solution {
2public:
3    bool isThereAPath(vector<vector<int>>& grid) {
4        int rows = grid.size();
5        int cols = grid[0].size();
6        int totalSteps = rows + cols - 1;  // Total cells in any path from (0,0) to (m-1,n-1)
7      
8        // If total steps is odd, we can't have equal 0s and 1s
9        if (totalSteps & 1) {
10            return false;
11        }
12      
13        int targetOnes = totalSteps >> 1;  // We need exactly half 1s and half 0s
14      
15        // Memoization table: memo[row][col][onesCount] 
16        // -1: not computed, 0: false, 1: true
17        int memo[rows][cols][totalSteps];
18        memset(memo, -1, sizeof(memo));
19      
20        // DFS function to check if a valid path exists
21        // Parameters: current row, current column, count of 1s so far
22        function<bool(int, int, int)> dfs = [&](int row, int col, int onesCount) -> bool {
23            // Check if out of bounds
24            if (row >= rows || col >= cols) {
25                return false;
26            }
27          
28            // Update count of 1s with current cell
29            onesCount += grid[row][col];
30          
31            // Check memoization
32            if (memo[row][col][onesCount] != -1) {
33                return memo[row][col][onesCount];
34            }
35          
36            // Pruning: if we have too many 1s or too many 0s already
37            int stepsUsed = row + col + 1;  // Steps taken to reach current cell
38            int zerosCount = stepsUsed - onesCount;
39            if (onesCount > targetOnes || zerosCount > targetOnes) {
40                return false;
41            }
42          
43            // Check if we reached the destination
44            if (row == rows - 1 && col == cols - 1) {
45                return onesCount == targetOnes;
46            }
47          
48            // Try moving down or right
49            bool canReachTarget = dfs(row + 1, col, onesCount) || dfs(row, col + 1, onesCount);
50          
51            // Store result in memoization table
52            memo[row][col][onesCount] = canReachTarget;
53            return canReachTarget;
54        };
55      
56        // Start DFS from top-left corner with 0 ones counted
57        return dfs(0, 0, 0);
58    }
59};
60
1function isThereAPath(grid: number[][]): boolean {
2    const rows = grid.length;
3    const cols = grid[0].length;
4    const totalSteps = rows + cols - 1;  // Total cells in any path from (0,0) to (m-1,n-1)
5  
6    // If total steps is odd, we can't have equal 0s and 1s
7    if (totalSteps & 1) {
8        return false;
9    }
10  
11    const targetOnes = totalSteps >> 1;  // We need exactly half 1s and half 0s
12  
13    // Memoization table: memo[row][col][onesCount]
14    // Map key format: "row,col,onesCount"
15    const memo = new Map<string, boolean>();
16  
17    // DFS function to check if a valid path exists
18    // Parameters: current row, current column, count of 1s so far
19    const dfs = (row: number, col: number, onesCount: number): boolean => {
20        // Check if out of bounds
21        if (row >= rows || col >= cols) {
22            return false;
23        }
24      
25        // Update count of 1s with current cell
26        onesCount += grid[row][col];
27      
28        // Create memoization key
29        const key = `${row},${col},${onesCount}`;
30      
31        // Check memoization
32        if (memo.has(key)) {
33            return memo.get(key)!;
34        }
35      
36        // Pruning: if we have too many 1s or too many 0s already
37        const stepsUsed = row + col + 1;  // Steps taken to reach current cell
38        const zerosCount = stepsUsed - onesCount;
39        if (onesCount > targetOnes || zerosCount > targetOnes) {
40            memo.set(key, false);
41            return false;
42        }
43      
44        // Check if we reached the destination
45        if (row === rows - 1 && col === cols - 1) {
46            const result = onesCount === targetOnes;
47            memo.set(key, result);
48            return result;
49        }
50      
51        // Try moving down or right
52        const canReachTarget = dfs(row + 1, col, onesCount) || dfs(row, col + 1, onesCount);
53      
54        // Store result in memoization table
55        memo.set(key, canReachTarget);
56        return canReachTarget;
57    };
58  
59    // Start DFS from top-left corner with 0 ones counted
60    return dfs(0, 0, 0);
61}
62

Time and Space Complexity

Time Complexity: O(m × n × (m + n))

The time complexity is determined by the memoized DFS function. The @cache decorator ensures each unique state (i, j, k) is computed only once.

  • i can take values from 0 to m-1 (m possible values)
  • j can take values from 0 to n-1 (n possible values)
  • k represents the count of 1s encountered so far. In the worst case, k can range from 0 to min(i+j, m+n-1). Since we're traversing from (0,0) to (m-1,n-1), the maximum value of k is bounded by the total path length m+n-1

Therefore, the total number of unique states is O(m × n × (m + n)).

Space Complexity: O(m × n × (m + n))

The space complexity consists of:

  • Memoization cache: The @cache decorator stores all computed states (i, j, k), which requires O(m × n × (m + n)) space
  • Recursion stack: In the worst case, the recursion depth can go up to m + n - 1 (the length of the path from top-left to bottom-right), contributing O(m + n) to space complexity

The dominant factor is the memoization cache, giving us an overall space complexity of O(m × n × (m + n)).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Path Length Calculation

A common mistake is forgetting that the path length is m + n - 1, not m + n. When moving from (0,0) to (m-1, n-1), we make exactly m-1 down moves and n-1 right moves, visiting a total of (m-1) + (n-1) + 1 = m + n - 1 cells.

Wrong approach:

path_length = rows + cols  # Incorrect!

Correct approach:

path_length = rows + cols - 1  # Correct

2. Forgetting to Include Starting Cell Value

Some implementations forget to count the value at the starting position (0,0) in their initial call. The grid value at the starting position must be included in the count.

Wrong approach:

# Starting without considering grid[0][0]
return dfs(0, 0, 0)  # This is actually correct, but only because
                     # we add grid[row][col] inside dfs

Alternative correct approach if counting outside:

# If you count before entering the cell
return dfs(0, 0, grid[0][0])

3. Inefficient Pruning or No Pruning

Without proper pruning conditions, the algorithm explores many unnecessary paths. The key insight is that if we've already collected too many 1s or 0s to possibly balance them by the destination, we should stop exploring that path.

Inefficient approach:

def dfs(row, col, ones_count):
    if row >= rows or col >= cols:
        return False
  
    ones_count += grid[row][col]
  
    # No pruning - explores all paths even when impossible
    if row == rows - 1 and col == cols - 1:
        return ones_count == target_ones
  
    return dfs(row + 1, col, ones_count) or dfs(row, col + 1, ones_count)

Optimized approach with pruning:

def dfs(row, col, ones_count):
    if row >= rows or col >= cols:
        return False
  
    ones_count += grid[row][col]
  
    # Pruning conditions
    zeros_count = row + col + 1 - ones_count
    if ones_count > target_ones or zeros_count > target_ones:
        return False  # Early termination
  
    if row == rows - 1 and col == cols - 1:
        return ones_count == target_ones
  
    return dfs(row + 1, col, ones_count) or dfs(row, col + 1, ones_count)

4. Missing Memoization Leading to Time Limit Exceeded

Without memoization, the same states (row, col, ones_count) are computed multiple times, leading to exponential time complexity.

Wrong approach:

# No @cache decorator or manual memoization
def dfs(row, col, ones_count):
    # ... rest of the code

Correct approach:

@cache  # Critical for performance!
def dfs(row, col, ones_count):
    # ... rest of the code

5. Integer Division vs Floor Division

When calculating the target count, using regular division / instead of integer division // or bit shifting can cause issues.

Potentially problematic:

target_ones = path_length / 2  # Returns float in Python 3

Correct approaches:

target_ones = path_length // 2  # Integer division
# OR
target_ones = path_length >> 1  # Bit shift (more efficient)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How does quick sort divide the problem into subproblems?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More