Facebook Pixel

2174. Remove All Ones With Row and Column Flips II πŸ”’

MediumBit ManipulationBreadth-First SearchArrayMatrix
Leetcode Link

Problem Description

You have a binary matrix (containing only 0s and 1s) with m rows and n columns. Your goal is to remove all 1s from the matrix using the minimum number of operations.

In each operation, you can:

  1. Choose any cell position (i, j) where the cell contains a 1
  2. Set all cells in row i to 0
  3. Set all cells in column j to 0

This means when you select a cell with value 1, you clear the entire row and column that cell belongs to.

For example, if you have a 3x3 matrix:

1 0 1
1 1 0
0 1 1

If you choose cell (1, 1) which contains a 1, after the operation the matrix becomes:

1 0 0
0 0 0
0 0 0

The row at index 1 and column at index 1 are both cleared.

The problem asks you to find the minimum number of such operations needed to make all cells in the matrix equal to 0.

The solution uses a BFS approach with bit manipulation to represent the grid state. Each cell's value is encoded as a bit in an integer, allowing efficient state tracking. The algorithm explores all possible operation sequences to find the shortest path from the initial state to a state where all bits are 0.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: This problem can be modeled as a graph where each state (configuration of the grid) is a node, and edges connect states that can be reached through a single operation.

Is it a tree?

  • No: The state graph is not a tree - multiple paths can lead to the same state (different sequences of operations can produce the same grid configuration).

Is the problem related to directed acyclic graphs?

  • No: The state graph is not necessarily acyclic, and the problem isn't about DAG properties.

Is the problem related to shortest paths?

  • Yes: We need to find the minimum number of operations (shortest path) from the initial state to the target state (all zeros).

Is the graph Weighted?

  • No: Each operation has the same cost (1 step), making this an unweighted graph problem.

BFS

  • Yes: For unweighted shortest path problems, BFS is the optimal choice.

Conclusion: The flowchart suggests using BFS (Breadth-First Search) for finding the minimum number of operations needed to clear all 1s from the grid. This aligns perfectly with the solution approach, which uses BFS to explore all possible states level by level, ensuring the first time we reach the target state (all zeros), we've found the minimum number of operations.

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

Intuition

When we perform an operation on a cell containing a 1, we clear its entire row and column. The key insight is that the order of operations doesn't matter for the final result - we just need to find the minimum number of operations to clear everything.

This problem is essentially asking for the shortest transformation sequence from the initial grid state to an all-zero state. Each possible grid configuration can be thought of as a state, and performing an operation transforms one state into another. This naturally forms a graph where:

  • Each node represents a unique grid configuration
  • Each edge represents one operation that transforms one configuration to another

Since we want the minimum number of operations, we're looking for the shortest path in this state graph. Because each operation has the same "cost" (counts as 1 operation), this becomes an unweighted shortest path problem, which BFS handles optimally.

The challenge is representing states efficiently. With small grid sizes, we can use bit manipulation - encoding the entire grid as a single integer where each bit represents whether a cell contains a 1 or 0. For an m x n grid, cell (i, j) maps to bit position i * n + j. This allows us to:

  1. Compactly represent any grid state as a single number
  2. Efficiently check if we've seen a state before using a set
  3. Quickly apply operations using bitwise operations (clearing a row/column becomes clearing specific bits)

The BFS explores states level by level:

  • Level 0: Initial grid state
  • Level 1: All states reachable with 1 operation
  • Level 2: All states reachable with 2 operations
  • And so on...

When we first encounter the state 0 (all cells cleared), we've found the minimum number of operations needed. The bit manipulation makes state transitions fast - to apply an operation at (i, j), we clear all bits corresponding to row i and column j using bitwise AND with inverted masks.

Learn more about Breadth-First Search patterns.

Solution Approach

The solution implements BFS with bit manipulation to track grid states efficiently. Here's how the implementation works:

1. State Encoding:

state = sum(1 << (i * n + j) for i in range(m) for j in range(n) if grid[i][j])

We encode the initial grid into a single integer. Each cell (i, j) with value 1 sets the bit at position i * n + j. This creates a unique integer representation of the grid.

2. BFS Initialization:

q = deque([state])
vis = {state}
ans = 0
  • q: Queue for BFS, starting with the initial state
  • vis: Set to track visited states and avoid cycles
  • ans: Counter for the number of operations (BFS levels)

3. Level-by-Level BFS:

while q:
    for _ in range(len(q)):
        state = q.popleft()
        if state == 0:
            return ans

We process all states at the current level before moving to the next. If we reach state 0 (all bits cleared), we return the current level number as the minimum operations.

4. Generating Next States: For each current state, we try all possible operations:

for i in range(m):
    for j in range(n):
        if grid[i][j] == 0:
            continue

We only consider cells that originally contained 1 in the input grid (valid operation positions).

5. Applying Operations:

nxt = state
for r in range(m):
    nxt &= ~(1 << (r * n + j))
for c in range(n):
    nxt &= ~(1 << (i * n + c))
  • Start with the current state
  • Clear all bits in column j: For each row r, we use ~(1 << (r * n + j)) to create a mask with all 1s except at position (r, j), then AND it with nxt
  • Clear all bits in row i: Similarly, clear all positions in row i

6. State Exploration:

if nxt not in vis:
    vis.add(nxt)
    q.append(nxt)

If we haven't seen this new state before, we add it to the queue for exploration in the next level.

7. Level Increment:

ans += 1

After processing all states at the current level, we increment the operation count.

The algorithm guarantees finding the minimum number of operations because BFS explores states in order of distance from the initial state. The first time we reach state 0, we've found the shortest path.

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 to illustrate the solution approach.

Consider a 2Γ—2 matrix:

1 1
0 1

Step 1: Encode the initial state

  • Cell (0,0) = 1 β†’ bit position 0Γ—2+0 = 0
  • Cell (0,1) = 1 β†’ bit position 0Γ—2+1 = 1
  • Cell (1,0) = 0 β†’ bit position 1Γ—2+0 = 2
  • Cell (1,1) = 1 β†’ bit position 1Γ—2+1 = 3

Initial state = 1011 in binary = 11 in decimal

Step 2: Begin BFS

  • Queue: [11]
  • Visited: {11}
  • Operations: 0

Step 3: Process Level 0 (initial state) State = 11 (1011)

  • Try operation at (0,0):

    • Clear row 0: bits 0,1 β†’ 1011 & 1100 = 1000
    • Clear col 0: bits 0,2 β†’ 1000 & 1011 = 1000
    • New state = 8 (1000)
  • Try operation at (0,1):

    • Clear row 0: bits 0,1 β†’ 1011 & 1100 = 1000
    • Clear col 1: bits 1,3 β†’ 1000 & 0101 = 0000
    • New state = 0 (0000) βœ“
  • Try operation at (1,1):

    • Clear row 1: bits 2,3 β†’ 1011 & 0011 = 0011
    • Clear col 1: bits 1,3 β†’ 0011 & 0101 = 0001
    • New state = 1 (0001)

Step 4: Move to Level 1

  • Queue: [8, 0, 1]
  • Visited: {11, 8, 0, 1}
  • Operations: 1

Step 5: Process Level 1 First state in queue is 8, but we encounter state 0 (all zeros) which means we've cleared the entire matrix!

Result: Minimum operations = 1

We can verify this: choosing cell (0,1) clears row 0 and column 1, which removes all 1s from the matrix in a single operation.

Solution Implementation

1class Solution:
2    def removeOnes(self, grid: List[List[int]]) -> int:
3        """
4        Remove all 1s from a binary grid by selecting cells and removing their entire row and column.
5        Returns minimum number of operations needed.
6      
7        Args:
8            grid: 2D binary matrix with 0s and 1s
9          
10        Returns:
11            Minimum operations to clear all 1s, or -1 if impossible
12        """
13        rows, cols = len(grid), len(grid[0])
14      
15        # Encode initial grid state as bitmask
16        # Each bit represents a cell: bit at position (i * cols + j) represents grid[i][j]
17        initial_state = sum(
18            1 << (i * cols + j) 
19            for i in range(rows) 
20            for j in range(cols) 
21            if grid[i][j] == 1
22        )
23      
24        # BFS queue and visited set
25        queue = deque([initial_state])
26        visited = {initial_state}
27        operations = 0
28      
29        # BFS to find minimum operations
30        while queue:
31            # Process all states at current level
32            level_size = len(queue)
33            for _ in range(level_size):
34                current_state = queue.popleft()
35              
36                # Check if all 1s are removed
37                if current_state == 0:
38                    return operations
39              
40                # Try removing each cell that contains a 1
41                for row in range(rows):
42                    for col in range(cols):
43                        # Skip if original cell was 0
44                        if grid[row][col] == 0:
45                            continue
46                      
47                        # Calculate next state after removing row and column
48                        next_state = current_state
49                      
50                        # Remove all cells in the same row
51                        for c in range(cols):
52                            next_state &= ~(1 << (row * cols + c))
53                      
54                        # Remove all cells in the same column
55                        for r in range(rows):
56                            next_state &= ~(1 << (r * cols + col))
57                      
58                        # Add to queue if not visited
59                        if next_state not in visited:
60                            visited.add(next_state)
61                            queue.append(next_state)
62          
63            operations += 1
64      
65        # No solution found
66        return -1
67
1class Solution {
2    public int removeOnes(int[][] grid) {
3        int rows = grid.length;
4        int cols = grid[0].length;
5      
6        // Convert the grid to a bitmask representation
7        // Each 1 in the grid is represented as a set bit in the state
8        int initialState = 0;
9        for (int row = 0; row < rows; row++) {
10            for (int col = 0; col < cols; col++) {
11                if (grid[row][col] == 1) {
12                    // Set the bit at position (row * cols + col)
13                    initialState |= 1 << (row * cols + col);
14                }
15            }
16        }
17      
18        // BFS setup: queue for states and visited set
19        Deque<Integer> queue = new ArrayDeque<>();
20        queue.offer(initialState);
21        Set<Integer> visited = new HashSet<>();
22        visited.add(initialState);
23      
24        int steps = 0;
25      
26        // BFS to find minimum steps to clear all 1s
27        while (!queue.isEmpty()) {
28            int levelSize = queue.size();
29          
30            // Process all states at current level
31            for (int i = 0; i < levelSize; i++) {
32                int currentState = queue.poll();
33              
34                // Check if we've cleared all 1s
35                if (currentState == 0) {
36                    return steps;
37                }
38              
39                // Try removing each row and column combination
40                for (int row = 0; row < rows; row++) {
41                    for (int col = 0; col < cols; col++) {
42                        // Only consider positions that had 1 in original grid
43                        if (grid[row][col] == 0) {
44                            continue;
45                        }
46                      
47                        int nextState = currentState;
48                      
49                        // Clear all bits in the selected row
50                        for (int r = 0; r < rows; r++) {
51                            nextState &= ~(1 << (r * cols + col));
52                        }
53                      
54                        // Clear all bits in the selected column
55                        for (int c = 0; c < cols; c++) {
56                            nextState &= ~(1 << (row * cols + c));
57                        }
58                      
59                        // Add new state if not visited
60                        if (!visited.contains(nextState)) {
61                            visited.add(nextState);
62                            queue.offer(nextState);
63                        }
64                    }
65                }
66            }
67            steps++;
68        }
69      
70        // Should not reach here if grid is valid
71        return -1;
72    }
73}
74
1class Solution {
2public:
3    int removeOnes(vector<vector<int>>& grid) {
4        int rows = grid.size();
5        int cols = grid[0].size();
6      
7        // Convert the grid to a bitmask representation
8        // Each cell (i, j) is represented by bit at position (i * cols + j)
9        int initialState = 0;
10        for (int row = 0; row < rows; ++row) {
11            for (int col = 0; col < cols; ++col) {
12                if (grid[row][col] == 1) {
13                    initialState |= (1 << (row * cols + col));
14                }
15            }
16        }
17      
18        // BFS setup: queue for states to explore and set for visited states
19        queue<int> bfsQueue{{initialState}};
20        unordered_set<int> visitedStates{{initialState}};
21      
22        int steps = 0;
23      
24        // BFS to find minimum steps to clear all 1s
25        while (!bfsQueue.empty()) {
26            int currentLevelSize = bfsQueue.size();
27          
28            // Process all states at current BFS level
29            for (int i = 0; i < currentLevelSize; ++i) {
30                int currentState = bfsQueue.front();
31                bfsQueue.pop();
32              
33                // Check if we've cleared all 1s
34                if (currentState == 0) {
35                    return steps;
36                }
37              
38                // Try removing each row and column combination
39                for (int row = 0; row < rows; ++row) {
40                    for (int col = 0; col < cols; ++col) {
41                        // Skip if the original grid cell is 0
42                        if (grid[row][col] == 0) {
43                            continue;
44                        }
45                      
46                        // Create next state by removing all 1s in row 'row' and column 'col'
47                        int nextState = currentState;
48                      
49                        // Clear all bits in the selected row
50                        for (int c = 0; c < cols; ++c) {
51                            nextState &= ~(1 << (row * cols + c));
52                        }
53                      
54                        // Clear all bits in the selected column
55                        for (int r = 0; r < rows; ++r) {
56                            nextState &= ~(1 << (r * cols + col));
57                        }
58                      
59                        // Add to queue if this state hasn't been visited
60                        if (visitedStates.find(nextState) == visitedStates.end()) {
61                            visitedStates.insert(nextState);
62                            bfsQueue.push(nextState);
63                        }
64                    }
65                }
66            }
67          
68            ++steps;
69        }
70      
71        // Should not reach here if the problem guarantees a solution
72        return -1;
73    }
74};
75
1function removeOnes(grid: number[][]): number {
2    const rows = grid.length;
3    const cols = grid[0].length;
4  
5    // Convert the grid to a bitmask representation
6    // Each cell (i, j) is represented by bit at position (i * cols + j)
7    let initialState = 0;
8    for (let row = 0; row < rows; row++) {
9        for (let col = 0; col < cols; col++) {
10            if (grid[row][col] === 1) {
11                initialState |= (1 << (row * cols + col));
12            }
13        }
14    }
15  
16    // BFS setup: queue for states to explore and set for visited states
17    const bfsQueue: number[] = [initialState];
18    const visitedStates = new Set<number>([initialState]);
19  
20    let steps = 0;
21  
22    // BFS to find minimum steps to clear all 1s
23    while (bfsQueue.length > 0) {
24        const currentLevelSize = bfsQueue.length;
25      
26        // Process all states at current BFS level
27        for (let i = 0; i < currentLevelSize; i++) {
28            const currentState = bfsQueue.shift()!;
29          
30            // Check if we've cleared all 1s
31            if (currentState === 0) {
32                return steps;
33            }
34          
35            // Try removing each row and column combination
36            for (let row = 0; row < rows; row++) {
37                for (let col = 0; col < cols; col++) {
38                    // Skip if the original grid cell is 0
39                    if (grid[row][col] === 0) {
40                        continue;
41                    }
42                  
43                    // Create next state by removing all 1s in row 'row' and column 'col'
44                    let nextState = currentState;
45                  
46                    // Clear all bits in the selected row
47                    for (let c = 0; c < cols; c++) {
48                        nextState &= ~(1 << (row * cols + c));
49                    }
50                  
51                    // Clear all bits in the selected column
52                    for (let r = 0; r < rows; r++) {
53                        nextState &= ~(1 << (r * cols + col));
54                    }
55                  
56                    // Add to queue if this state hasn't been visited
57                    if (!visitedStates.has(nextState)) {
58                        visitedStates.add(nextState);
59                        bfsQueue.push(nextState);
60                    }
61                }
62            }
63        }
64      
65        steps++;
66    }
67  
68    // Should not reach here if the problem guarantees a solution
69    return -1;
70}
71

Time and Space Complexity

Time Complexity: O(2^(m*n) * m^2 * n^2)

The algorithm uses BFS to explore different states of the grid. Each state is represented as a bitmask where each bit indicates whether a cell contains a 1.

  • The total number of possible states is 2^(m*n) since each of the m*n cells can be either 0 or 1.
  • For each state, we iterate through all m*n positions to find cells with value 1.
  • For each cell with value 1 at position (i,j), we clear all bits in row i and column j, which takes O(m + n) operations.
  • In the worst case, we visit all possible states, and for each state, we perform O(m*n*(m+n)) operations.
  • This simplifies to O(2^(m*n) * m*n * (m+n)) which can be written as O(2^(m*n) * m^2 * n^2) when considering the dominant terms.

Space Complexity: O(2^(m*n))

  • The vis set can store up to 2^(m*n) different states in the worst case.
  • The queue q can also contain up to O(2^(m*n)) states in the worst case.
  • The space for storing each state is constant since we use an integer bitmask.
  • Therefore, the overall space complexity is O(2^(m*n)).

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

Common Pitfalls

1. Attempting Operations on Cells That Were Originally 0

The Pitfall: A common mistake is trying to perform operations on cells that currently contain 1 in the transformed state, rather than cells that originally contained 1 in the input grid. During BFS, as we apply operations and create new states, some cells that were originally 1 become 0. However, according to the problem, we can only choose cells that contain 1 at the time of the operation.

Incorrect Approach:

# Wrong: Checking current state instead of original grid
for row in range(rows):
    for col in range(cols):
        # This checks if the cell in current state has a 1
        if (current_state >> (row * cols + col)) & 1:
            # Apply operation...

Why This Fails: The problem states you can only choose a cell position (i, j) where the cell contains a 1. This means we need to check if the position has a 1 in the current state, not just whether it was a valid operation position in the original grid.

Correct Solution:

for row in range(rows):
    for col in range(cols):
        # First check if this was originally a valid position
        if grid[row][col] == 0:
            continue
      
        # Then check if current state has a 1 at this position
        if not ((current_state >> (row * cols + col)) & 1):
            continue
          
        # Now we can safely apply the operation
        next_state = current_state
        # Clear row and column...

2. Inefficient State Representation for Large Grids

The Pitfall: Using bit manipulation with a single integer limits the grid size to roughly 60-64 cells (depending on Python's integer implementation). For larger grids, the bit positions exceed what can be represented.

Example of the Problem:

# For a 10x10 grid, we need 100 bits
# Position (9, 9) would require bit position 99
state = 1 << 99  # This might cause issues in some contexts

Solution for Larger Grids: For grids larger than what fits in a single integer, consider alternative representations:

# Option 1: Use tuple of tuples for state representation
def grid_to_state(grid):
    return tuple(tuple(row) for row in grid)

# Option 2: Use string representation
def grid_to_state(grid):
    return ''.join(''.join(str(cell) for cell in row) for row in grid)

# Option 3: Keep only positions of 1s as a frozenset
def grid_to_state(grid):
    return frozenset((i, j) for i in range(len(grid)) 
                     for j in range(len(grid[0])) if grid[i][j] == 1)

3. Not Handling Edge Cases Properly

The Pitfall: Failing to handle special cases like empty grids or grids with no 1s.

Problematic Code:

# This might fail if grid is already all zeros
initial_state = sum(1 << (i * cols + j) for i in range(rows) 
                   for j in range(cols) if grid[i][j] == 1)
# If grid has no 1s, initial_state = 0, but we still enter BFS

Correct Handling:

# Check if grid is already clear
initial_state = sum(1 << (i * cols + j) for i in range(rows) 
                   for j in range(cols) if grid[i][j] == 1)

if initial_state == 0:
    return 0  # No operations needed

# Continue with BFS...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following array represent a max heap?


Recommended Readings

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

Load More