2123. Minimum Operations to Remove Adjacent Ones in Matrix π
Problem Description
You have a binary matrix (containing only 0s and 1s) where you can flip any 1 to become a 0. Your goal is to make the matrix "well-isolated" using the minimum number of flips.
A matrix is considered well-isolated when no two 1s are adjacent to each other horizontally or vertically (4-directionally connected). In other words, every 1 in the matrix should be completely surrounded by 0s in the four cardinal directions (up, down, left, right).
For example:
- A matrix with 1s at positions that share an edge (horizontally or vertically adjacent) is NOT well-isolated
- A matrix where all 1s are separated by at least one 0 in all four directions IS well-isolated
- 1s that are diagonally adjacent are allowed (since we only consider 4-directional connectivity)
The problem asks you to find the minimum number of 1s that need to be flipped to 0 to achieve this well-isolated state.
The solution uses a bipartite matching approach by treating the grid as a checkerboard pattern where positions with (i + j) % 2 == 0
form one set and positions with (i + j) % 2 == 1
form another set. This works because adjacent cells always belong to different sets in this checkerboard pattern. The algorithm then finds the maximum matching between adjacent 1s in these two sets, which represents the minimum number of 1s that need to be removed to break all connections.
Intuition
The key insight is recognizing that this problem is about breaking connections between adjacent 1s. When two 1s are adjacent, we must flip at least one of them to make the grid well-isolated.
Think of the grid like a checkerboard where we color cells black and white alternately. Any cell at position (i, j)
where (i + j)
is even gets one color, and cells where (i + j)
is odd get another color. The crucial observation is that adjacent cells always have different colors in this checkerboard pattern.
Now, consider pairs of adjacent 1s in the grid. Since adjacent cells have different colors, every pair of connected 1s forms an edge between a black cell and a white cell. This naturally creates a bipartite graph where:
- One set contains all 1s on black cells
- Another set contains all 1s on white cells
- Edges exist between adjacent 1s
To make the grid well-isolated, we need to break all these edges. Breaking an edge means flipping at least one of the two 1s it connects. The question becomes: what's the minimum number of 1s to flip to break all edges?
This is exactly the Minimum Vertex Cover problem in bipartite graphs - finding the smallest set of vertices (1s to flip) such that every edge (connection between adjacent 1s) has at least one endpoint in this set.
By KΓΆnig's theorem, in a bipartite graph, the size of the minimum vertex cover equals the size of the maximum matching. A matching pairs up vertices from the two sets such that no vertex appears in more than one pair. The maximum matching gives us the optimal way to "cover" the most edges with the fewest vertices.
Therefore, we can solve this by finding the maximum matching in the bipartite graph formed by adjacent 1s, which directly gives us the minimum number of flips needed.
Learn more about Graph patterns.
Solution Approach
The implementation uses the Hungarian Algorithm (specifically, the augmenting path method) to find the maximum bipartite matching.
Step 1: Build the Bipartite Graph
We iterate through the grid and only consider cells where (i + j) % 2 == 1
(one partition of our checkerboard). For each 1 in this partition, we:
- Convert the 2D position
(i, j)
to a 1D index:x = i * n + j
- Check all four adjacent cells (up, down, left, right)
- If an adjacent cell contains a 1, we add an edge in our graph
g
for i, row in enumerate(grid):
for j, v in enumerate(row):
if (i + j) % 2 and v: # Only process 1s in odd-sum positions
x = i * n + j
# Check all 4 directions and add edges to adjacent 1s
Step 2: Initialize Matching Array
We create a match
array of size m * n
initialized to -1
. This array tracks which vertex from the even-sum partition is matched to each vertex from the odd-sum partition.
Step 3: Find Maximum Matching
For each vertex in our graph (1s at odd-sum positions), we try to find an augmenting path using DFS:
def find(i: int) -> int:
for j in g[i]: # Try all adjacent 1s
if j not in vis: # Not visited in current DFS
vis.add(j)
if match[j] == -1 or find(match[j]): # j is unmatched or can be rematched
match[j] = i
return 1
return 0
The find
function implements the augmenting path search:
- It tries to match vertex
i
with any of its neighborsj
- If
j
is unmatched (match[j] == -1
), we directly match them - If
j
is already matched to some other vertex, we recursively try to find an alternative match for that vertex - The
vis
set prevents cycles during a single augmenting path search
Step 4: Count the Matches
For each vertex in the odd-sum partition with edges, we call find
and accumulate successful matches:
ans = 0
for i in g.keys():
vis = set() # Reset visited set for each new augmenting path search
ans += find(i)
The final answer ans
represents the maximum matching size, which by KΓΆnig's theorem equals the minimum vertex cover - the minimum number of 1s we need to flip to make the grid well-isolated.
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 this 3Γ3 binary matrix:
1 1 0 1 1 1 0 1 0
Step 1: Identify the checkerboard pattern
We color cells based on (i + j) % 2
:
- Even sum (0): positions (0,0), (0,2), (1,1), (2,0), (2,2)
- Odd sum (1): positions (0,1), (1,0), (1,2), (2,1)
E O E (E = even, O = odd)
O E O
E O E
Our matrix with the pattern overlay:
1(E) 1(O) 0(E) 1(O) 1(E) 1(O) 0(E) 1(O) 0(E)
Step 2: Build the bipartite graph
We process 1s at odd-sum positions and find their adjacent 1s:
-
Position (0,1) has value 1, checks neighbors:
- Up: out of bounds
- Down: (1,1) has value 1 β
- Left: (0,0) has value 1 β
- Right: (0,2) has value 0
- Adds edges: (0,1) β {(1,1), (0,0)}
-
Position (1,0) has value 1, checks neighbors:
- Up: (0,0) has value 1 β
- Down: (2,0) has value 0
- Left: out of bounds
- Right: (1,1) has value 1 β
- Adds edges: (1,0) β {(0,0), (1,1)}
-
Position (1,2) has value 1, checks neighbors:
- Up: (0,2) has value 0
- Down: (2,2) has value 0
- Left: (1,1) has value 1 β
- Right: out of bounds
- Adds edges: (1,2) β {(1,1)}
-
Position (2,1) has value 1, checks neighbors:
- Up: (1,1) has value 1 β
- Down: out of bounds
- Left: (2,0) has value 0
- Right: (2,2) has value 0
- Adds edges: (2,1) β {(1,1)}
Converting to 1D indices (i * 3 + j):
- (0,1) = 1, (1,0) = 3, (1,2) = 5, (2,1) = 7 [odd-sum vertices]
- (0,0) = 0, (1,1) = 4 [even-sum vertices they connect to]
Graph g:
- g[1] = {4, 0}
- g[3] = {0, 4}
- g[5] = {4}
- g[7] = {4}
Step 3: Find maximum matching using augmenting paths
Initialize match array: match = [-1, -1, -1, -1, -1, -1, -1, -1, -1]
Process vertex 1:
- Try to match with 4: unmatched, so
match[4] = 1
β - Return 1
Process vertex 3:
- Try to match with 0: unmatched, so
match[0] = 3
β - Return 1
Process vertex 5:
- Try to match with 4: already matched to 1
- Try to rematch 1: can it find another match?
- Vertex 1 tries 0: already matched to 3
- Try to rematch 3: can it find another match?
- Vertex 3 tries 4: visited in this path
- Return 0
- Return 0
- Return 0
Process vertex 7:
- Try to match with 4: already matched to 1
- Try to rematch 1: can it find another match?
- Vertex 1 tries 0: already matched to 3
- Try to rematch 3: can it find another match?
- Vertex 3 tries 4: visited in this path
- Return 0
- Return 0
- Return 0
Step 4: Result
Maximum matching size = 2 (vertices 1β4 and 3β0 are matched)
This means we need to flip at least 2 ones to make the grid well-isolated. Looking at our original grid, the matching tells us an optimal solution: we can flip the 1s at positions (0,0) and (1,1), which breaks all adjacencies:
Original:
1 1 0 1 1 1 0 1 0
After flipping (0,0) and (1,1):
0 1 0 1 0 1 0 1 0
Now no two 1s are adjacent - the grid is well-isolated with 2 flips, which matches our algorithm's answer.
Solution Implementation
1class Solution:
2 def minimumOperations(self, grid: List[List[int]]) -> int:
3 def find_augmenting_path(source_node: int) -> int:
4 """
5 Find an augmenting path from source_node using DFS.
6 Returns 1 if a matching is found, 0 otherwise.
7 """
8 # Try to find an unmatched neighbor or augment existing matching
9 for neighbor in adjacency_list[source_node]:
10 if neighbor not in visited:
11 visited.add(neighbor)
12 # If neighbor is unmatched or we can find an augmenting path from its match
13 if matching[neighbor] == -1 or find_augmenting_path(matching[neighbor]):
14 matching[neighbor] = source_node
15 return 1
16 return 0
17
18 # Build adjacency list for bipartite graph
19 adjacency_list = defaultdict(list)
20 rows, cols = len(grid), len(grid[0])
21
22 # Process each cell in the grid
23 for row_idx, row in enumerate(grid):
24 for col_idx, value in enumerate(row):
25 # Only process cells where (row + col) is odd and cell value is non-zero
26 # This creates one partition of the bipartite graph
27 if (row_idx + col_idx) % 2 and value:
28 # Convert 2D coordinates to 1D index
29 current_cell = row_idx * cols + col_idx
30
31 # Check bottom neighbor
32 if row_idx < rows - 1 and grid[row_idx + 1][col_idx]:
33 adjacency_list[current_cell].append(current_cell + cols)
34
35 # Check top neighbor
36 if row_idx and grid[row_idx - 1][col_idx]:
37 adjacency_list[current_cell].append(current_cell - cols)
38
39 # Check right neighbor
40 if col_idx < cols - 1 and grid[row_idx][col_idx + 1]:
41 adjacency_list[current_cell].append(current_cell + 1)
42
43 # Check left neighbor
44 if col_idx and grid[row_idx][col_idx - 1]:
45 adjacency_list[current_cell].append(current_cell - 1)
46
47 # Initialize matching array (-1 means unmatched)
48 matching = [-1] * (rows * cols)
49 max_matching = 0
50
51 # Find maximum matching using augmenting paths
52 for source_node in adjacency_list.keys():
53 visited = set()
54 max_matching += find_augmenting_path(source_node)
55
56 return max_matching
57
1class Solution {
2 // Adjacency list for the bipartite graph
3 private Map<Integer, List<Integer>> adjacencyList = new HashMap<>();
4 // Visited set for DFS traversal during augmenting path search
5 private Set<Integer> visited = new HashSet<>();
6 // Array to store matching information (matched pairs)
7 private int[] matchedPartner;
8
9 public int minimumOperations(int[][] grid) {
10 int rows = grid.length;
11 int cols = grid[0].length;
12
13 // Build bipartite graph by connecting adjacent cells with value 1
14 // Only process cells where (row + col) is odd to create bipartite sets
15 for (int row = 0; row < rows; ++row) {
16 for (int col = 0; col < cols; ++col) {
17 // Check if current cell belongs to the first set of bipartite graph
18 // and has value 1
19 if ((row + col) % 2 == 1 && grid[row][col] == 1) {
20 // Convert 2D coordinates to 1D index
21 int currentNode = row * cols + col;
22
23 // Check all four adjacent cells and add edges if they have value 1
24 // Check bottom neighbor
25 if (row < rows - 1 && grid[row + 1][col] == 1) {
26 adjacencyList.computeIfAbsent(currentNode, key -> new ArrayList<>())
27 .add(currentNode + cols);
28 }
29 // Check top neighbor
30 if (row > 0 && grid[row - 1][col] == 1) {
31 adjacencyList.computeIfAbsent(currentNode, key -> new ArrayList<>())
32 .add(currentNode - cols);
33 }
34 // Check right neighbor
35 if (col < cols - 1 && grid[row][col + 1] == 1) {
36 adjacencyList.computeIfAbsent(currentNode, key -> new ArrayList<>())
37 .add(currentNode + 1);
38 }
39 // Check left neighbor
40 if (col > 0 && grid[row][col - 1] == 1) {
41 adjacencyList.computeIfAbsent(currentNode, key -> new ArrayList<>())
42 .add(currentNode - 1);
43 }
44 }
45 }
46 }
47
48 // Initialize matching array with -1 (indicating no match)
49 matchedPartner = new int[rows * cols];
50 Arrays.fill(matchedPartner, -1);
51
52 // Find maximum bipartite matching using augmenting path algorithm
53 int maxMatching = 0;
54 for (int node : adjacencyList.keySet()) {
55 // Try to find an augmenting path starting from each unmatched node
56 maxMatching += find(node);
57 // Clear visited set for next iteration
58 visited.clear();
59 }
60
61 return maxMatching;
62 }
63
64 /**
65 * Finds an augmenting path using DFS from the given starting node
66 * @param startNode the node to start searching from
67 * @return 1 if an augmenting path is found, 0 otherwise
68 */
69 private int find(int startNode) {
70 // Explore all adjacent nodes
71 for (int adjacentNode : adjacencyList.get(startNode)) {
72 // Only process unvisited nodes in current DFS
73 if (visited.add(adjacentNode)) {
74 // If adjacent node is unmatched or we can find an augmenting path
75 // from its current match, then we found an augmenting path
76 if (matchedPartner[adjacentNode] == -1 ||
77 find(matchedPartner[adjacentNode]) == 1) {
78 // Update the matching
79 matchedPartner[adjacentNode] = startNode;
80 return 1;
81 }
82 }
83 }
84 // No augmenting path found
85 return 0;
86 }
87}
88
1class Solution {
2public:
3 int minimumOperations(vector<vector<int>>& grid) {
4 int rows = grid.size();
5 int cols = grid[0].size();
6
7 // Store matching information: match[node] = matched partner or -1 if unmatched
8 vector<int> match(rows * cols, -1);
9
10 // Track visited nodes during DFS for augmenting paths
11 unordered_set<int> visited;
12
13 // Adjacency list representation of the bipartite graph
14 // Only store edges from odd-indexed cells (one partition of bipartite graph)
15 unordered_map<int, vector<int>> adjacencyList;
16
17 // Build the bipartite graph
18 // Cells with (row + col) % 2 == 1 form one partition
19 // Connect them to adjacent cells with value 1
20 for (int row = 0; row < rows; ++row) {
21 for (int col = 0; col < cols; ++col) {
22 // Check if current cell belongs to odd partition and has value 1
23 if ((row + col) % 2 == 1 && grid[row][col] == 1) {
24 int currentNode = row * cols + col;
25
26 // Check bottom neighbor
27 if (row < rows - 1 && grid[row + 1][col] == 1) {
28 adjacencyList[currentNode].push_back(currentNode + cols);
29 }
30
31 // Check top neighbor
32 if (row > 0 && grid[row - 1][col] == 1) {
33 adjacencyList[currentNode].push_back(currentNode - cols);
34 }
35
36 // Check right neighbor
37 if (col < cols - 1 && grid[row][col + 1] == 1) {
38 adjacencyList[currentNode].push_back(currentNode + 1);
39 }
40
41 // Check left neighbor
42 if (col > 0 && grid[row][col - 1] == 1) {
43 adjacencyList[currentNode].push_back(currentNode - 1);
44 }
45 }
46 }
47 }
48
49 int totalMatches = 0;
50
51 // DFS function to find augmenting path from node sourceNode
52 function<int(int)> findAugmentingPath = [&](int sourceNode) -> int {
53 // Try to match sourceNode with each of its neighbors
54 for (int& neighbor : adjacencyList[sourceNode]) {
55 // Skip if neighbor already visited in current DFS
56 if (!visited.count(neighbor)) {
57 visited.insert(neighbor);
58
59 // If neighbor is unmatched or we can find an augmenting path
60 // from its current match, then we can match sourceNode with neighbor
61 if (match[neighbor] == -1 || findAugmentingPath(match[neighbor])) {
62 match[neighbor] = sourceNode;
63 return 1;
64 }
65 }
66 }
67 return 0;
68 };
69
70 // Find maximum matching using augmenting paths
71 for (auto& [node, _] : adjacencyList) {
72 totalMatches += findAugmentingPath(node);
73 visited.clear(); // Clear visited set for next iteration
74 }
75
76 return totalMatches;
77 }
78};
79
1/**
2 * Finds the minimum number of operations needed using bipartite matching
3 * @param grid - 2D grid of numbers
4 * @returns The minimum number of operations
5 */
6function minimumOperations(grid: number[][]): number {
7 const rows = grid.length;
8 const cols = grid[0].length;
9
10 // Array to store matching pairs, -1 means unmatched
11 const matchedNodes: number[] = Array(rows * cols).fill(-1);
12
13 // Set to track visited nodes during DFS
14 const visitedNodes: Set<number> = new Set();
15
16 // Adjacency list to store graph connections
17 const adjacencyList: Map<number, number[]> = new Map();
18
19 // Build bipartite graph for cells where (row + col) is odd and cell value is non-zero
20 for (let row = 0; row < rows; ++row) {
21 for (let col = 0; col < cols; ++col) {
22 // Only process cells with odd sum of coordinates and non-zero value
23 if ((row + col) % 2 && grid[row][col]) {
24 const currentNodeId = row * cols + col;
25 adjacencyList.set(currentNodeId, []);
26
27 // Check bottom neighbor
28 if (row < rows - 1 && grid[row + 1][col]) {
29 adjacencyList.get(currentNodeId)!.push(currentNodeId + cols);
30 }
31
32 // Check top neighbor
33 if (row && grid[row - 1][col]) {
34 adjacencyList.get(currentNodeId)!.push(currentNodeId - cols);
35 }
36
37 // Check right neighbor
38 if (col < cols - 1 && grid[row][col + 1]) {
39 adjacencyList.get(currentNodeId)!.push(currentNodeId + 1);
40 }
41
42 // Check left neighbor
43 if (col && grid[row][col - 1]) {
44 adjacencyList.get(currentNodeId)!.push(currentNodeId - 1);
45 }
46 }
47 }
48 }
49
50 /**
51 * Finds an augmenting path using DFS for bipartite matching
52 * @param sourceNode - Starting node for path finding
53 * @returns 1 if augmenting path found, 0 otherwise
54 */
55 const findAugmentingPath = (sourceNode: number): number => {
56 // Try to find a match for each adjacent node
57 for (const adjacentNode of adjacencyList.get(sourceNode)!) {
58 if (!visitedNodes.has(adjacentNode)) {
59 visitedNodes.add(adjacentNode);
60
61 // If adjacent node is unmatched or we can find alternate path for its match
62 if (matchedNodes[adjacentNode] === -1 || findAugmentingPath(matchedNodes[adjacentNode])) {
63 matchedNodes[adjacentNode] = sourceNode;
64 return 1;
65 }
66 }
67 }
68 return 0;
69 };
70
71 let totalMatches = 0;
72
73 // Find maximum matching by attempting to match each node in the bipartite set
74 for (const nodeId of adjacencyList.keys()) {
75 totalMatches += findAugmentingPath(nodeId);
76 visitedNodes.clear();
77 }
78
79 return totalMatches;
80}
81
Time and Space Complexity
Time Complexity: O(m * n * E)
where m
and n
are the dimensions of the grid, and E
is the number of edges in the bipartite graph (which is O(m * n)
in the worst case).
- Building the graph
g
takesO(m * n)
time as we iterate through each cell once and check at most 4 neighbors for each cell. - The matching algorithm (Hungarian/Ford-Fulkerson for bipartite matching) runs with:
- Outer loop iterates through all keys in
g
, which is at mostO(m * n / 2)
(only cells where(i + j) % 2
is true andv
is non-zero) - For each key, the
find()
function performs a DFS that can visit at mostO(m * n)
nodes in the worst case - Each edge is explored at most once per DFS call due to the
vis
set
- Outer loop iterates through all keys in
- Overall:
O(m * n) + O(m * n) * O(m * n) = O((m * n)^2)
Space Complexity: O(m * n)
- Graph
g
stores adjacency lists with total edgesO(m * n)
in worst case match
array usesO(m * n)
spacevis
set uses at mostO(m * n)
space during each DFS call- The recursion stack for
find()
can go up toO(m * n)
depth in the worst case - Overall space complexity:
O(m * n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Bipartite Graph Construction
A common mistake is building edges from both partitions of the checkerboard, which would create duplicate edges and lead to incorrect matching results.
Wrong approach:
# Processing ALL cells with value 1
for i in range(rows):
for j in range(cols):
if grid[i][j] == 1: # Wrong: processes both partitions
# Add edges to all adjacent 1s
Correct approach:
# Only process cells where (i + j) % 2 == 1
for i in range(rows):
for j in range(cols):
if (i + j) % 2 == 1 and grid[i][j] == 1: # Correct: single partition
# Add edges to adjacent 1s
2. Reusing the Visited Set Across Multiple Augmenting Path Searches
Each augmenting path search needs a fresh visited set. Reusing the same set would prevent finding valid augmenting paths in subsequent iterations.
Wrong approach:
visited = set() # Single visited set for all searches
for source_node in adjacency_list.keys():
max_matching += find_augmenting_path(source_node) # Wrong: visited never cleared
Correct approach:
for source_node in adjacency_list.keys():
visited = set() # Fresh visited set for each search
max_matching += find_augmenting_path(source_node)
3. Misunderstanding the Problem Requirements
Some might think we need to flip ALL 1s that have adjacent 1s, but we only need to flip the MINIMUM number to break all adjacencies.
Example:
1 1 1 β We only need to flip the middle 1: 1 0 1
Not all three 1s need to be flipped!
4. Incorrect Index Conversion Between 2D and 1D
When converting between 2D grid coordinates and 1D indices, using the wrong dimension for multiplication causes incorrect neighbor identification.
Wrong approach:
current_cell = row_idx * rows + col_idx # Wrong: should use cols, not rows
Correct approach:
current_cell = row_idx * cols + col_idx # Correct: multiply by number of columns
5. Not Handling Edge Cases in Boundary Checks
Forgetting to check grid boundaries before accessing neighbors can cause index out of bounds errors.
Wrong approach:
# Checking neighbor without boundary validation if grid[row_idx + 1][col_idx]: # Can cause IndexError adjacency_list[current_cell].append(current_cell + cols)
Correct approach:
# Check boundary first if row_idx < rows - 1 and grid[row_idx + 1][col_idx]: adjacency_list[current_cell].append(current_cell + cols)
Which of the following shows the order of node visit in a Breadth-first Search?
Recommended Readings
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
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!