2174. Remove All Ones With Row and Column Flips II π
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:
- Choose any cell position
(i, j)
where the cell contains a 1 - Set all cells in row
i
to 0 - 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.
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:
- Compactly represent any grid state as a single number
- Efficiently check if we've seen a state before using a set
- 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 statevis
: Set to track visited states and avoid cyclesans
: 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 rowr
, we use~(1 << (r * n + j))
to create a mask with all 1s except at position(r, j)
, then AND it withnxt
- Clear all bits in row
i
: Similarly, clear all positions in rowi
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 EvaluatorExample 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
)
- Clear row 0: bits 0,1 β
-
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
) β
- Clear row 0: bits 0,1 β
-
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
)
- Clear row 1: bits 2,3 β
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 them*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 rowi
and columnj
, which takesO(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 asO(2^(m*n) * m^2 * n^2)
when considering the dominant terms.
Space Complexity: O(2^(m*n))
- The
vis
set can store up to2^(m*n)
different states in the worst case. - The queue
q
can also contain up toO(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...
Which of the following array represent a max heap?
Recommended Readings
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
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!