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.
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:
- If we've already collected more 1s than the target count
s = (m + n - 1) / 2
, we can stop exploring this path - 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 targets
, 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 usings & 1
), returnfalse
immediately since we can't split an odd number equally - If even, set
s = s / 2
(usings >>= 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 gridk
tracks the number of 1s collected so far
For each recursive call:
-
Boundary check: If we go out of bounds (
i >= m
orj >= n
), returnfalse
-
Update count: Add the current cell's value to our 1s count:
k += grid[i][j]
-
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
- If
-
Base case: If we reach the destination
(i == m-1 and j == n-1)
, check ifk == s
(exactly half 1s means exactly half 0s too) -
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
- Move down:
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 EvaluatorExample 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 from0
tom-1
(m possible values)j
can take values from0
ton-1
(n possible values)k
represents the count of 1s encountered so far. In the worst case,k
can range from0
tomin(i+j, m+n-1)
. Since we're traversing from(0,0)
to(m-1,n-1)
, the maximum value ofk
is bounded by the total path lengthm+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 requiresO(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), contributingO(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)
How does quick sort divide the problem into subproblems?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!