2732. Find a Good Subset of the Matrix
Problem Description
You are given a 0-indexed m x n
binary matrix grid
where each element is either 0 or 1.
The task is to find a good subset of rows from this matrix. A subset of rows is considered good if it satisfies the following condition:
- For every column in the selected subset of rows, the sum of that column's values must be at most half of the number of rows in the subset.
- Mathematically, if you select
k
rows, then for each column, the sum of thek
values in that column must be at mostfloor(k/2)
.
For example:
- If you select 1 row, each column sum must be at most
floor(1/2) = 0
- If you select 2 rows, each column sum must be at most
floor(2/2) = 1
- If you select 3 rows, each column sum must be at most
floor(3/2) = 1
- If you select 4 rows, each column sum must be at most
floor(4/2) = 2
You need to return an array containing the row indices of any good subset, sorted in ascending order. If multiple good subsets exist, you can return any one of them. If no good subset exists, return an empty array.
The key insight from the solution is that only subsets of size 1 or 2 can possibly be good:
- For
k = 1
: The only way to satisfy the condition is if the selected row contains all 0s (since each column sum must be 0). - For
k = 2
: The two selected rows must have no column where both rows have a 1 (their bitwise AND must be 0), ensuring each column sum is at most 1. - For
k ≥ 3
: It can be proven that no valid subset exists due to the constraints on column sums and the limited number of columns (at most 5).
Intuition
The key insight comes from analyzing what values of k
(number of selected rows) can actually satisfy the constraint.
Let's think about this systematically. For a subset of k
rows to be good, each column sum must be at most floor(k/2)
. This means:
- When
k = 1
: column sum ≤ 0, so the row must be all zeros - When
k = 2
: column sum ≤ 1, so at most one 1 per column - When
k = 3
: column sum ≤ 1, still at most one 1 per column - When
k = 4
: column sum ≤ 2, so at most two 1s per column
Here's the crucial observation: if we can't find a valid subset for k = 2
, we definitely can't find one for k = 3
. Why? Because with 3 rows and the same constraint (column sum ≤ 1), we need an even stricter condition - essentially, the three rows must be "compatible" in a way that no column has more than one 1. But if no two rows satisfy this condition, adding a third row won't help.
For k = 4
, even though the constraint relaxes to allowing 2 ones per column, we run into a counting problem. If no two rows have bitwise AND equal to 0 (meaning every pair of rows has at least one column where both have 1s), then when we pick 4 rows, we're choosing C(4,2) = 6
pairs. Each pair contributes at least one column with sum ≥ 2. Since the matrix has at most 5 columns, by the pigeonhole principle, at least one column must have sum > 2, violating our constraint.
This pattern continues for larger k
values. The constraints become impossible to satisfy when we can't even find 2 rows that work together.
Therefore, we only need to check:
- Is there a single row with all zeros?
- Are there two rows whose bitwise AND equals 0?
The solution cleverly uses bit manipulation where each row is represented as a bitmask (integer), making it efficient to check if two rows have no overlapping 1s by testing if (mask1 & mask2) == 0
.
Solution Approach
The solution implements the case analysis strategy by checking for valid subsets of size 1 and 2.
Step 1: Convert rows to bitmasks and check for all-zero rows
We iterate through each row and convert it to a bitmask representation. For each row i
:
- Initialize
mask = 0
- For each column
j
with valuex
, we set thej
-th bit of the mask usingmask |= x << j
- If
mask == 0
, it means the entire row contains only zeros, so we immediately return[i]
as this single row forms a good subset
Step 2: Store unique masks with their row indices
We use a dictionary g
where:
- Key: the bitmask representation of a row
- Value: the row index
This ensures we only keep one row index for each unique bit pattern, which is sufficient since we only need to find any valid subset.
Step 3: Find two rows with bitwise AND equal to 0
We perform a nested iteration over all pairs of masks in the dictionary:
- For each pair
(a, i)
and(b, j)
wherea
andb
are masks andi
,j
are row indices - Check if
(a & b) == 0
, which means the two rows have no column where both have a 1 - If found, return the sorted pair
[i, j]
Step 4: Return empty array if no valid subset found
If we've checked all possibilities and found no valid subset, return an empty array.
Time Complexity: O(m × n + U²)
where m
is the number of rows, n
is the number of columns, and U
is the number of unique row patterns (at most 2^n
).
Space Complexity: O(U)
for storing the dictionary of unique masks.
The brilliance of this approach lies in recognizing that we only need to consider small subset sizes and using bit manipulation for efficient checking of row compatibility.
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 concrete example to illustrate the solution approach.
Consider the following 4×3 binary matrix:
grid = [[0,1,1], [1,0,1], [0,0,0], [1,1,0]]
Step 1: Convert rows to bitmasks and check for all-zero rows
We process each row and convert it to a bitmask:
- Row 0:
[0,1,1]
→ mask =0|1<<1|1<<2
=0|2|4
=6
(binary: 110) - Row 1:
[1,0,1]
→ mask =1<<0|0|1<<2
=1|0|4
=5
(binary: 101) - Row 2:
[0,0,0]
→ mask =0|0|0
=0
(binary: 000)- Found an all-zero row! Return
[2]
immediately.
- Found an all-zero row! Return
The algorithm would stop here and return [2]
as the answer, since row 2 contains all zeros and forms a valid subset of size 1.
Alternative scenario: Let's modify the example to see the full algorithm:
grid = [[0,1,1], [1,0,1], [1,0,0], [1,1,0]]
Step 1: Convert and check for zeros
- Row 0:
[0,1,1]
→ mask =6
(binary: 110) - Row 1:
[1,0,1]
→ mask =5
(binary: 101) - Row 2:
[1,0,0]
→ mask =1
(binary: 001) - Row 3:
[1,1,0]
→ mask =3
(binary: 011)
No all-zero rows found, so we continue.
Step 2: Store unique masks
Dictionary g
:
6
→ row index0
5
→ row index1
1
→ row index2
3
→ row index3
Step 3: Find two compatible rows
Check all pairs for (mask1 & mask2) == 0
:
- Masks
6
and5
:110 & 101 = 100
(not 0) - Masks
6
and1
:110 & 001 = 000
✓ Found a valid pair!- Rows 0 and 2 have no overlapping 1s
- Return sorted indices:
[0, 2]
Let's verify this answer:
- Selected rows:
[0,1,1]
and[1,0,0]
- Column sums:
[0+1=1, 1+0=1, 1+0=1]
- All column sums ≤ floor(2/2) = 1 ✓
The algorithm successfully found a good subset of size 2.
Solution Implementation
1class Solution:
2 def goodSubsetofBinaryMatrix(self, grid: List[List[int]]) -> List[int]:
3 # Dictionary to store bitmask representation of each row
4 # Key: bitmask, Value: row index
5 bitmask_to_row = {}
6
7 # Convert each row to a bitmask representation
8 for row_index, row in enumerate(grid):
9 bitmask = 0
10
11 # Build bitmask where bit j is set if row[j] == 1
12 for col_index, value in enumerate(row):
13 bitmask |= value << col_index
14
15 # If the row contains all zeros, it's a valid subset by itself
16 if bitmask == 0:
17 return [row_index]
18
19 # Store the mapping of bitmask to row index
20 bitmask_to_row[bitmask] = row_index
21
22 # Check all pairs of rows to find two that have no overlapping 1s
23 for bitmask_a, row_index_a in bitmask_to_row.items():
24 for bitmask_b, row_index_b in bitmask_to_row.items():
25 # If bitwise AND is 0, the two rows have no column with both having 1
26 if (bitmask_a & bitmask_b) == 0:
27 return sorted([row_index_a, row_index_b])
28
29 # No valid subset found
30 return []
31
1class Solution {
2 public List<Integer> goodSubsetofBinaryMatrix(int[][] grid) {
3 // Map to store bitmask representation of each row and its index
4 // Key: bitmask of the row, Value: row index
5 Map<Integer, Integer> rowMaskToIndex = new HashMap<>();
6
7 // Process each row in the grid
8 for (int rowIndex = 0; rowIndex < grid.length; rowIndex++) {
9 // Convert current row to a bitmask representation
10 // Each bit position represents a column, bit is 1 if grid[i][j] is 1
11 int rowMask = 0;
12 for (int colIndex = 0; colIndex < grid[0].length; colIndex++) {
13 rowMask |= grid[rowIndex][colIndex] << colIndex;
14 }
15
16 // If the row contains all zeros, it forms a valid subset by itself
17 if (rowMask == 0) {
18 return List.of(rowIndex);
19 }
20
21 // Store the bitmask and corresponding row index
22 rowMaskToIndex.put(rowMask, rowIndex);
23 }
24
25 // Check all pairs of rows to find two rows with no overlapping 1s
26 for (Map.Entry<Integer, Integer> firstEntry : rowMaskToIndex.entrySet()) {
27 for (Map.Entry<Integer, Integer> secondEntry : rowMaskToIndex.entrySet()) {
28 // If bitwise AND is 0, no column has 1 in both rows
29 if ((firstEntry.getKey() & secondEntry.getKey()) == 0) {
30 int firstRowIndex = firstEntry.getValue();
31 int secondRowIndex = secondEntry.getValue();
32 // Return indices in ascending order
33 return List.of(Math.min(firstRowIndex, secondRowIndex),
34 Math.max(firstRowIndex, secondRowIndex));
35 }
36 }
37 }
38
39 // No valid subset found
40 return List.of();
41 }
42}
43
1class Solution {
2public:
3 vector<int> goodSubsetofBinaryMatrix(vector<vector<int>>& grid) {
4 // Map to store bitmask -> row index
5 // The bitmask represents the binary values in each row
6 unordered_map<int, int> maskToRowIndex;
7
8 // Iterate through each row in the grid
9 for (int rowIdx = 0; rowIdx < grid.size(); ++rowIdx) {
10 // Build bitmask for current row
11 // Each bit position represents a column value (0 or 1)
12 int rowMask = 0;
13 for (int colIdx = 0; colIdx < grid[0].size(); ++colIdx) {
14 rowMask |= grid[rowIdx][colIdx] << colIdx;
15 }
16
17 // If row contains all zeros, it's a valid single-row subset
18 if (rowMask == 0) {
19 return {rowIdx};
20 }
21
22 // Store the row index for this bitmask
23 maskToRowIndex[rowMask] = rowIdx;
24 }
25
26 // Check all pairs of different bitmasks
27 for (auto& [firstMask, firstRowIdx] : maskToRowIndex) {
28 for (auto& [secondMask, secondRowIdx] : maskToRowIndex) {
29 // If bitwise AND is 0, these two rows form a valid subset
30 // (No column has 1 in both rows)
31 if ((firstMask & secondMask) == 0) {
32 // Return indices in ascending order
33 return {min(firstRowIdx, secondRowIdx), max(firstRowIdx, secondRowIdx)};
34 }
35 }
36 }
37
38 // No valid subset found
39 return {};
40 }
41};
42
1/**
2 * Finds a good subset of rows from a binary matrix where each column sum is at most 1
3 * @param grid - A binary matrix (2D array of 0s and 1s)
4 * @returns An array of row indices forming a good subset, or empty array if none exists
5 */
6function goodSubsetofBinaryMatrix(grid: number[][]): number[] {
7 // Map to store bitmask representations of rows and their indices
8 const rowMaskToIndex: Map<number, number> = new Map();
9 const rowCount = grid.length;
10 const columnCount = grid[0].length;
11
12 // Process each row and convert it to a bitmask
13 for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
14 let rowMask = 0;
15
16 // Create bitmask where bit j is set if grid[rowIndex][j] is 1
17 for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) {
18 rowMask |= grid[rowIndex][columnIndex] << columnIndex;
19 }
20
21 // If row has all zeros, it forms a valid subset by itself
22 if (rowMask === 0) {
23 return [rowIndex];
24 }
25
26 // Store the bitmask and its corresponding row index
27 rowMaskToIndex.set(rowMask, rowIndex);
28 }
29
30 // Check all pairs of rows to find two rows with no overlapping 1s
31 for (const [firstMask, firstRowIndex] of rowMaskToIndex.entries()) {
32 for (const [secondMask, secondRowIndex] of rowMaskToIndex.entries()) {
33 // If bitwise AND is 0, these rows have no common 1s in any column
34 if ((firstMask & secondMask) === 0) {
35 // Return indices in ascending order
36 return [Math.min(firstRowIndex, secondRowIndex), Math.max(firstRowIndex, secondRowIndex)];
37 }
38 }
39 }
40
41 // No valid subset found
42 return [];
43}
44
Time and Space Complexity
Time Complexity: O(m × n + 4^n)
The time complexity consists of two main parts:
O(m × n)
: Building the bitmask for each row requires iterating through allm
rows and for each row, iterating through alln
columns to construct the mask.O(4^n)
: In the worst case, we need to check all pairs of masks stored in the dictionaryg
. Since each column can either be 0 or 1, and we're storing unique masks, there can be at most2^n
different masks. Checking all pairs would beO((2^n)^2) = O(4^n)
.
Space Complexity: O(2^n)
The space complexity is determined by the dictionary g
which stores the mapping from masks to row indices. Since each mask is an n-bit number representing a unique combination of 0s and 1s across n columns, there can be at most 2^n
different masks stored in the dictionary.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Checking the Same Row Twice
Issue: The nested loop checks all pairs including when bitmask_a
and bitmask_b
refer to the same row. When a row is paired with itself, the condition (bitmask_a & bitmask_b) == 0
would only be true if the row contains all zeros, but we've already handled that case. This creates an edge case where the same row index could appear twice in the result.
Example: If we have a row of all zeros that wasn't caught in the first check (though this shouldn't happen with correct implementation), the code might return [i, i]
which is invalid.
Solution: Add a check to ensure we're not comparing a row with itself:
for bitmask_a, row_index_a in bitmask_to_row.items():
for bitmask_b, row_index_b in bitmask_to_row.items():
# Skip if comparing the same row
if row_index_a >= row_index_b:
continue
if (bitmask_a & bitmask_b) == 0:
return sorted([row_index_a, row_index_b])
Pitfall 2: Overwriting Row Indices in the Dictionary
Issue: Multiple rows might have the same bitmask pattern. The current implementation overwrites the previous row index when encountering a duplicate pattern, potentially missing valid pairs.
Example:
- Row 0:
[1, 0, 0]
→ bitmask = 1 - Row 2:
[1, 0, 0]
→ bitmask = 1 (overwrites row 0) - Row 3:
[0, 1, 1]
→ bitmask = 6
The code would miss the valid pair [0, 3]
because row 0's index was overwritten.
Solution: Store all row indices for each bitmask pattern:
# Dictionary to store all row indices for each bitmask
bitmask_to_rows = defaultdict(list)
for row_index, row in enumerate(grid):
bitmask = 0
for col_index, value in enumerate(row):
bitmask |= value << col_index
if bitmask == 0:
return [row_index]
bitmask_to_rows[bitmask].append(row_index)
# Check pairs from different bitmask groups
for bitmask_a, rows_a in bitmask_to_rows.items():
for bitmask_b, rows_b in bitmask_to_rows.items():
if (bitmask_a & bitmask_b) == 0:
# Return the first valid pair
if bitmask_a != bitmask_b:
return sorted([rows_a[0], rows_b[0]])
Pitfall 3: Inefficient Duplicate Pair Checking
Issue: The nested loop checks pairs (a, b)
and later (b, a)
, which is redundant since bitwise AND is commutative.
Solution: Optimize by only checking each unique pair once:
masks_list = list(bitmask_to_row.items())
for i in range(len(masks_list)):
for j in range(i, len(masks_list)):
bitmask_a, row_index_a = masks_list[i]
bitmask_b, row_index_b = masks_list[j]
if (bitmask_a & bitmask_b) == 0:
if i == j: # Same bitmask group
continue
return sorted([row_index_a, row_index_b])
Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?
Recommended Readings
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
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!