LeetCode 2174 - Remove All Ones With Row and Column Flips II
The problem gives us an m x n binary matrix where every cell contains either 0 or 1. Our goal is to remove all 1s using the minimum number of operations. An operation can only be performed on a cell (i, j) that currently contains a 1.
Difficulty: 🟡 Medium
Topics: Array, Bit Manipulation, Breadth-First Search, Matrix
Solution
Problem Understanding
The problem gives us an m x n binary matrix where every cell contains either 0 or 1. Our goal is to remove all 1s using the minimum number of operations.
An operation can only be performed on a cell (i, j) that currently contains a 1. Once we choose such a cell, we immediately set every cell in row i and every cell in column j to 0.
This is important because the operation affects an entire row and an entire column simultaneously. The chosen cell acts as the "anchor" of the operation, but the effect spreads across both dimensions.
The output should be the minimum number of operations required so that the entire matrix becomes all zeros.
The constraints are the key to solving this problem efficiently:
1 <= m, n <= 151 <= m * n <= 15
Although the matrix dimensions can individually be as large as 15, the total number of cells is at most 15. This is a very strong hint that we should think about representing the entire matrix as a bitmask.
Since there are at most 15 cells, the total number of possible matrix states is at most:
2^15 = 32768
That is small enough for exhaustive state exploration using BFS or memoized DFS.
Several edge cases are important:
- The matrix may already contain all zeros, in which case the answer is
0. - A matrix may contain only a single
1, which requires exactly one operation. - Different operation orders may produce different numbers of steps, so greedy strategies are unreliable.
- Performing an operation can remove many
1s at once, especially in dense matrices. - Since operations are only allowed on cells currently equal to
1, we cannot arbitrarily choose any row and column combination.
These observations naturally lead us toward state-space search.
Approaches
Brute Force Approach
A naive brute-force strategy would recursively try every possible valid operation from the current matrix state.
At each step:
- Scan the matrix for all cells containing
1. - For each such cell, simulate the operation.
- Recurse on the resulting matrix.
- Track the minimum number of operations among all possibilities.
This approach is correct because it explores every possible sequence of operations. Eventually, it will discover the optimal sequence.
However, the problem is that many matrix states repeat through different operation orders. Without remembering previously computed states, the recursion tree grows exponentially and performs massive redundant work.
Even though the total state count is only 2^15, naive recursion may revisit the same state many times.
Key Insight
The crucial observation is that the entire matrix can be represented as a bitmask because there are at most 15 cells.
Each unique matrix configuration becomes a node in a graph:
- A node represents one matrix state.
- An edge represents performing one valid operation.
Since every operation has equal cost, the shortest path from the initial state to the all-zero state can be found using Breadth-First Search.
BFS is ideal here because:
- Each operation costs exactly
1. - BFS guarantees the first time we reach the all-zero state is with the minimum number of operations.
- The total number of states is small enough to explore exhaustively.
We also avoid repeated work by storing visited states.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | Exponential, potentially much larger than 2^(mn) due to repeated states |
Exponential recursion stack | Explores all operation sequences redundantly |
| Optimal BFS with Bitmask | O(2^(mn) * mn * mn) |
O(2^(mn)) |
Explores each state once using BFS |
Algorithm Walkthrough
Step 1: Encode the Matrix as a Bitmask
We flatten the matrix into a single integer.
For every cell:
- If
grid[i][j] == 1, we set the corresponding bit in the mask. - If it is
0, the bit remains unset.
For example:
grid = [
[1,0],
[1,1]
]
can become:
bit positions:
0 1
2 3
mask = 1101₂
Using bitmasks makes state transitions extremely efficient.
Step 2: Initialize BFS
We start BFS from the initial mask.
We maintain:
- A queue storing
(state, steps) - A visited set to avoid revisiting states
The target state is simply:
0
because all bits cleared means all matrix cells are zero.
Step 3: Process Each BFS State
For the current mask:
- If the mask is
0, return the current step count. - Otherwise, examine every cell in the matrix.
For each cell (i, j):
- Check whether its bit is set.
- If not set, we cannot perform an operation there.
- If set, simulate the operation.
Step 4: Simulate an Operation
When choosing (i, j):
- Every bit in row
ibecomes0 - Every bit in column
jbecomes0
We create a new mask by clearing those bits.
This is done efficiently with bit operations.
Step 5: Push New States into BFS
If the resulting mask has not been visited:
- Mark it visited
- Push it into the queue with
steps + 1
Because BFS explores states level by level, the first time we reach state 0 is guaranteed to be optimal.
Why it works
BFS explores all states reachable in k operations before exploring states requiring k + 1 operations. Every edge in the state graph has equal weight because every operation costs exactly one step. Therefore, the first time BFS reaches the all-zero state, it must be using the minimum possible number of operations.
Python Solution
from collections import deque
from typing import List
class Solution:
def removeOnes(self, grid: List[List[int]]) -> int:
m = len(grid)
n = len(grid[0])
initial_mask = 0
for i in range(m):
for j in range(n):
if grid[i][j] == 1:
bit = i * n + j
initial_mask |= (1 << bit)
queue = deque([(initial_mask, 0)])
visited = {initial_mask}
while queue:
mask, steps = queue.popleft()
if mask == 0:
return steps
for i in range(m):
for j in range(n):
bit = i * n + j
if not (mask & (1 << bit)):
continue
next_mask = mask
# Clear entire row i
for col in range(n):
row_bit = i * n + col
next_mask &= ~(1 << row_bit)
# Clear entire column j
for row in range(m):
col_bit = row * n + j
next_mask &= ~(1 << col_bit)
if next_mask not in visited:
visited.add(next_mask)
queue.append((next_mask, steps + 1))
return -1
The implementation begins by converting the matrix into a compact integer bitmask. Each matrix cell maps to one bit position. This allows the entire matrix state to fit inside a single integer because there are at most fifteen cells.
The BFS queue stores pairs of (mask, steps), where mask represents the current matrix configuration and steps represents how many operations were used to reach it.
For each state, the algorithm checks every cell. If the corresponding bit is set, that means the cell currently contains a 1, so it is a valid operation target.
To simulate the operation, the code clears all bits in the chosen row and column. Bit clearing is performed using:
next_mask &= ~(1 << position)
This efficiently removes cells from the matrix state.
The visited set ensures every state is processed at most once. Since BFS processes states in increasing order of operations, the first time we encounter the all-zero state is guaranteed to be optimal.
Go Solution
package main
import "container/list"
func removeOnes(grid [][]int) int {
m := len(grid)
n := len(grid[0])
initialMask := 0
for i := 0; i < m; i++ {
for j := 0; j < n; j++ {
if grid[i][j] == 1 {
bit := i*n + j
initialMask |= (1 << bit)
}
}
}
type State struct {
mask int
steps int
}
queue := list.New()
queue.PushBack(State{initialMask, 0})
visited := map[int]bool{
initialMask: true,
}
for queue.Len() > 0 {
front := queue.Front()
queue.Remove(front)
current := front.Value.(State)
mask := current.mask
steps := current.steps
if mask == 0 {
return steps
}
for i := 0; i < m; i++ {
for j := 0; j < n; j++ {
bit := i*n + j
if (mask & (1 << bit)) == 0 {
continue
}
nextMask := mask
// Clear row i
for col := 0; col < n; col++ {
rowBit := i*n + col
nextMask &= ^(1 << rowBit)
}
// Clear column j
for row := 0; row < m; row++ {
colBit := row*n + j
nextMask &= ^(1 << colBit)
}
if !visited[nextMask] {
visited[nextMask] = true
queue.PushBack(State{nextMask, steps + 1})
}
}
}
}
return -1
}
The Go implementation follows the exact same BFS strategy as the Python version.
One notable difference is bit clearing syntax. In Go, the expression:
nextMask &= ^(1 << position)
is used instead of Python's ~.
The BFS queue is implemented using container/list, since Go does not provide a built-in deque structure. The visited structure uses a map[int]bool.
Because the maximum state count is only 32768, regular integer types are completely sufficient and there is no overflow concern.
Worked Examples
Example 1
Input:
[
[1,1,1],
[1,1,1],
[0,1,0]
]
Initial mask:
111111010
BFS Level 0
| State | Steps |
|---|---|
| 111111010 | 0 |
Suppose we choose cell (1,1).
Affected cells:
- Entire row 1
- Entire column 1
Resulting grid:
[
[1,0,1],
[0,0,0],
[0,0,0]
]
New mask:
101000000
BFS Level 1
| State | Steps |
|---|---|
| 101000000 | 1 |
Choose cell (0,0).
Affected:
- Row 0
- Column 0
Result:
[
[0,0,0],
[0,0,0],
[0,0,0]
]
Mask becomes:
000000000
Answer:
2
Example 2
Input:
[
[0,1,0],
[1,0,1],
[0,1,0]
]
Initial mask contains four active bits.
One optimal sequence:
- Choose
(1,0) - Choose
(2,1)
After the first operation:
[
[0,1,0],
[0,0,0],
[0,1,0]
]
After the second operation:
[
[0,0,0],
[0,0,0],
[0,0,0]
]
Total operations:
2
Example 3
Input:
[
[0,0],
[0,0]
]
Initial mask:
0
The BFS immediately returns 0 because the matrix already contains no 1s.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(2^(mn) * mn * mn) |
Each state examines all cells and may clear rows and columns |
| Space | O(2^(mn)) |
BFS queue and visited set store matrix states |
The maximum number of states is 2^(mn), and mn <= 15, so the total state space is at most 32768.
For each state, we iterate through every cell to find valid operations. For each valid operation, we may scan an entire row and column to clear bits. Since both dimensions are at most 15, this remains efficient enough.
Test Cases
from typing import List
class Solution:
def removeOnes(self, grid: List[List[int]]) -> int:
from collections import deque
m = len(grid)
n = len(grid[0])
initial_mask = 0
for i in range(m):
for j in range(n):
if grid[i][j]:
initial_mask |= (1 << (i * n + j))
queue = deque([(initial_mask, 0)])
visited = {initial_mask}
while queue:
mask, steps = queue.popleft()
if mask == 0:
return steps
for i in range(m):
for j in range(n):
bit = i * n + j
if not (mask & (1 << bit)):
continue
next_mask = mask
for col in range(n):
next_mask &= ~(1 << (i * n + col))
for row in range(m):
next_mask &= ~(1 << (row * n + j))
if next_mask not in visited:
visited.add(next_mask)
queue.append((next_mask, steps + 1))
return -1
solution = Solution()
assert solution.removeOnes([[1,1,1],[1,1,1],[0,1,0]]) == 2 # Provided example 1
assert solution.removeOnes([[0,1,0],[1,0,1],[0,1,0]]) == 2 # Provided example 2
assert solution.removeOnes([[0,0],[0,0]]) == 0 # Already empty matrix
assert solution.removeOnes([[1]]) == 1 # Single cell with one
assert solution.removeOnes([[0]]) == 0 # Single cell with zero
assert solution.removeOnes([[1,1,1,1]]) == 1 # Single row
assert solution.removeOnes([[1],[1],[1]]) == 1 # Single column
assert solution.removeOnes([[1,0],[0,1]]) == 2 # Diagonal ones
assert solution.removeOnes([[1,1],[1,1]]) == 1 # Entire matrix removable in one step
assert solution.removeOnes([[1,0,1],[0,1,0]]) == 2 # Sparse matrix
assert solution.removeOnes([[1,0,0],[0,0,0],[0,0,1]]) == 2 # Far apart ones
| Test | Why |
|---|---|
[[1,1,1],[1,1,1],[0,1,0]] |
Verifies dense matrix behavior |
[[0,1,0],[1,0,1],[0,1,0]] |
Verifies nontrivial optimal choices |
[[0,0],[0,0]] |
Checks already solved case |
[[1]] |
Smallest nonzero input |
[[0]] |
Smallest zero input |
[[1,1,1,1]] |
Tests single-row clearing |
[[1],[1],[1]] |
Tests single-column clearing |
[[1,0],[0,1]] |
Ensures disconnected ones handled correctly |
[[1,1],[1,1]] |
Verifies one-operation full clear |
[[1,0,1],[0,1,0]] |
Sparse configuration |
[[1,0,0],[0,0,0],[0,0,1]] |
Multiple isolated components |
Edge Cases
One important edge case is when the matrix already contains no 1s. A careless implementation might still enter the BFS loop and attempt unnecessary processing. In this solution, the initial mask becomes 0, and BFS immediately returns 0 operations.
Another important edge case is a matrix with only one row or one column. In these situations, a single operation can potentially clear every remaining 1 because the chosen row or column covers the entire matrix dimension. The implementation handles this naturally because row and column clearing logic is completely general.
A third subtle edge case is disconnected 1s that cannot be removed together efficiently. Greedy strategies often fail here because choosing the locally best operation may not minimize the global operation count. BFS avoids this issue by exploring all possible operation sequences level by level, guaranteeing optimality.
A final edge case involves repeated states reached through different operation orders. Without a visited set, the algorithm would revisit the same masks repeatedly and become extremely inefficient. The visited set ensures each state is processed exactly once.