2397. Maximum Rows Covered by Columns
Problem Description
You have a binary matrix (containing only 0s and 1s) with m
rows and n
columns. Your task is to select exactly numSelect
distinct columns from this matrix to maximize the number of rows that become "covered."
A row is considered covered when one of these conditions is met:
- Every cell with value
1
in that row belongs to one of your selected columns - The row contains no
1
s at all (all cells are0
)
For example, if a row is [1, 0, 1, 0]
and you select columns 0 and 2, this row is covered because both positions with 1
s (column 0 and column 2) are in your selection. However, if you only select column 0, the row is not covered because the 1
at column 2 is not included in your selection.
The goal is to find the best combination of numSelect
columns that maximizes the count of covered rows.
Key Points:
- You must select exactly
numSelect
columns (no more, no less) - A row with all
0
s is automatically covered regardless of column selection - A row with
1
s is only covered if ALL its1
s fall within selected columns - Return the maximum number of rows that can be covered
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: The problem involves a binary matrix and column selection, not nodes and edges as in graph problems.
Need to solve for kth smallest/largest?
- No: We're looking for the maximum number of covered rows, not the kth smallest or largest element.
Involves Linked Lists?
- No: The problem works with a 2D matrix, not linked list data structures.
Does the problem have small constraints?
- Yes: Looking at the problem, we need to select
numSelect
columns fromn
total columns. The matrix dimensions are typically small enough that we can enumerate all possible combinations of columns. Withn
columns, there areC(n, numSelect)
ways to choose columns, which is manageable for small values ofn
(typically ≤ 30).
Brute force / Backtracking?
- Yes: Since we have small constraints and need to explore all possible combinations of selecting
numSelect
columns fromn
columns to find the maximum coverage, a brute force or backtracking approach is suitable. We can enumerate all possible column selections and check which one covers the maximum number of rows.
Conclusion: The flowchart suggests using a brute force/backtracking approach. This aligns perfectly with the solution, which uses binary enumeration to try all possible column combinations and finds the one that maximizes row coverage.
Intuition
The key insight is recognizing that we need to try different combinations of columns and see which combination covers the most rows. Since we must select exactly numSelect
columns from n
total columns, we're essentially dealing with a combination problem.
Think of each row as having a "requirement" - it needs all its 1
positions to be covered by our selected columns. For a row like [1, 0, 1, 0]
, we need to select both column 0 and column 2 to cover it. If we miss even one column containing a 1
, the row isn't covered.
This naturally leads to a brute force approach: try every possible way to select numSelect
columns and count how many rows each selection covers. The challenge is efficiently checking all combinations and determining coverage.
Here's where bit manipulation becomes clever. We can represent each row as a binary number where bit j
is set if matrix[i][j] == 1
. For example, row [1, 0, 1, 0]
becomes 0101
in binary (or 5
in decimal). Similarly, our column selection can be represented as a bitmask where bit j
is set if we select column j
.
The beautiful part is that checking if a row is covered becomes a simple bitwise operation: if (row_mask & column_mask) == row_mask
, then all the 1
s in that row are covered by our selected columns. This works because the AND operation will preserve only the bits where both the row has a 1
and we've selected that column. If the result equals the original row mask, we know every required column was selected.
By iterating through all possible column selections (all numbers from 0
to 2^n - 1
), filtering for those with exactly numSelect
bits set, and counting covered rows for each valid selection, we can find the maximum coverage efficiently.
Learn more about Backtracking patterns.
Solution Approach
The implementation follows the binary enumeration strategy to explore all possible column selections:
Step 1: Convert Rows to Bitmasks
First, we transform each row of the matrix into a binary number. For each row, we create a bitmask where bit j
is set to 1
if matrix[i][j] == 1
:
rows = []
for row in matrix:
mask = reduce(or_, (1 << j for j, x in enumerate(row) if x), 0)
rows.append(mask)
Here, 1 << j
creates a number with only the j
-th bit set. The reduce(or_, ...)
combines all these bits using the OR operation. For example, if row is [1, 0, 1, 0]
, we get:
- Position 0 has
1
: contribute1 << 0 = 0001
- Position 2 has
1
: contribute1 << 2 = 0100
- Combined with OR:
0001 | 0100 = 0101
(binary) =5
(decimal)
Step 2: Enumerate All Column Selections
We iterate through all possible column selection schemes from 0
to 2^n - 1
, where n
is the number of columns:
for mask in range(1 << len(matrix[0])):
Each number in this range represents a different way to select columns. For instance, with 4 columns:
mask = 5
(binary0101
) means selecting columns 0 and 2mask = 3
(binary0011
) means selecting columns 0 and 1
Step 3: Filter Valid Selections
We only consider selections with exactly numSelect
columns:
if mask.bit_count() != numSelect: continue
The bit_count()
method counts the number of 1
bits in the mask, which corresponds to the number of selected columns.
Step 4: Count Covered Rows
For each valid column selection, we count how many rows are covered:
t = sum((x & mask) == x for x in rows)
The expression (x & mask) == x
checks if row x
is covered. The AND operation x & mask
keeps only the bits where both the row has a 1
AND we've selected that column. If this equals the original row x
, it means all required columns for that row were selected.
Step 5: Track Maximum Coverage
We maintain the maximum number of covered rows across all valid selections:
ans = max(ans, t)
The time complexity is O(2^n × m)
where n
is the number of columns and m
is the number of rows, since we check all 2^n
possible column selections and for each, we verify coverage for all m
rows.
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.
Given:
- Matrix:
[[0,0,0], [1,0,1], [0,1,1], [0,0,1]]
- numSelect:
2
(we must select exactly 2 columns)
Step 1: Convert Rows to Bitmasks
Let's convert each row to its bitmask representation:
- Row 0:
[0,0,0]
→ No 1s → bitmask =000
(binary) =0
(decimal) - Row 1:
[1,0,1]
→ 1s at positions 0 and 2 → bitmask =101
(binary) =5
(decimal) - Row 2:
[0,1,1]
→ 1s at positions 1 and 2 → bitmask =110
(binary) =6
(decimal) - Row 3:
[0,0,1]
→ 1 at position 2 → bitmask =100
(binary) =4
(decimal)
So our rows array becomes: [0, 5, 6, 4]
Step 2-3: Enumerate Valid Column Selections
With 3 columns total, we check masks from 0
to 7
(binary 111
). We only keep those with exactly 2 bits set:
mask = 3
(binary011
) → columns 0 and 1 selected ✓mask = 5
(binary101
) → columns 0 and 2 selected ✓mask = 6
(binary110
) → columns 1 and 2 selected ✓
Step 4: Count Covered Rows for Each Selection
Let's check each valid selection:
Selection 1: Columns 0 and 1 (mask = 011
= 3)
- Row 0 (mask
000
):000 & 011 = 000
, equals row mask? Yes ✓ (covered) - Row 1 (mask
101
):101 & 011 = 001
, equals row mask? No ✗ (not covered) - Row 2 (mask
110
):110 & 011 = 010
, equals row mask? No ✗ (not covered) - Row 3 (mask
100
):100 & 011 = 000
, equals row mask? No ✗ (not covered) - Total covered: 1 row
Selection 2: Columns 0 and 2 (mask = 101
= 5)
- Row 0 (mask
000
):000 & 101 = 000
, equals row mask? Yes ✓ (covered) - Row 1 (mask
101
):101 & 101 = 101
, equals row mask? Yes ✓ (covered) - Row 2 (mask
110
):110 & 101 = 100
, equals row mask? No ✗ (not covered) - Row 3 (mask
100
):100 & 101 = 100
, equals row mask? Yes ✓ (covered) - Total covered: 3 rows
Selection 3: Columns 1 and 2 (mask = 110
= 6)
- Row 0 (mask
000
):000 & 110 = 000
, equals row mask? Yes ✓ (covered) - Row 1 (mask
101
):101 & 110 = 100
, equals row mask? No ✗ (not covered) - Row 2 (mask
110
):110 & 110 = 110
, equals row mask? Yes ✓ (covered) - Row 3 (mask
100
):100 & 110 = 100
, equals row mask? Yes ✓ (covered) - Total covered: 3 rows
Step 5: Return Maximum
The maximum coverage is 3 rows, achieved by either selecting columns {0, 2} or columns {1, 2}.
Note how Row 0 with all zeros is always covered regardless of our column selection, while rows with 1s require all their 1-positions to be included in our selected columns to be covered.
Solution Implementation
1class Solution:
2 def maximumRows(self, matrix: List[List[int]], numSelect: int) -> int:
3 from functools import reduce
4 from operator import or_
5
6 # Convert each row to a bitmask where bit j is set if matrix[row][j] == 1
7 row_masks = []
8 for row in matrix:
9 # Create bitmask: if row[j] == 1, set bit j to 1
10 row_mask = reduce(or_, (1 << col_idx for col_idx, value in enumerate(row) if value == 1), 0)
11 row_masks.append(row_mask)
12
13 max_covered_rows = 0
14 num_columns = len(matrix[0])
15
16 # Try all possible combinations of selecting numSelect columns
17 for column_selection_mask in range(1 << num_columns):
18 # Check if exactly numSelect columns are selected
19 if column_selection_mask.bit_count() != numSelect:
20 continue
21
22 # Count how many rows are fully covered by selected columns
23 # A row is covered if all its 1s are in selected columns
24 # This means (row_mask & column_selection_mask) == row_mask
25 covered_rows_count = sum(
26 (row_mask & column_selection_mask) == row_mask
27 for row_mask in row_masks
28 )
29
30 # Update maximum covered rows found so far
31 max_covered_rows = max(max_covered_rows, covered_rows_count)
32
33 return max_covered_rows
34
1class Solution {
2 public int maximumRows(int[][] matrix, int numSelect) {
3 int numRows = matrix.length;
4 int numCols = matrix[0].length;
5
6 // Convert each row to a bitmask where bit j is set if matrix[row][j] == 1
7 int[] rowBitmasks = new int[numRows];
8 for (int row = 0; row < numRows; row++) {
9 for (int col = 0; col < numCols; col++) {
10 if (matrix[row][col] == 1) {
11 // Set the bit at position 'col' for this row's bitmask
12 rowBitmasks[row] |= (1 << col);
13 }
14 }
15 }
16
17 int maxCoveredRows = 0;
18
19 // Try all possible combinations of selecting 'numSelect' columns
20 // Each bit in 'columnMask' represents whether that column is selected
21 for (int columnMask = 1; columnMask < (1 << numCols); columnMask++) {
22 // Skip if the number of selected columns doesn't match numSelect
23 if (Integer.bitCount(columnMask) != numSelect) {
24 continue;
25 }
26
27 // Count how many rows are fully covered by the selected columns
28 int coveredRowCount = 0;
29 for (int rowBitmask : rowBitmasks) {
30 // A row is covered if all its 1s are in selected columns
31 // This means (rowBitmask & columnMask) should equal rowBitmask
32 if ((rowBitmask & columnMask) == rowBitmask) {
33 coveredRowCount++;
34 }
35 }
36
37 // Update the maximum number of covered rows
38 maxCoveredRows = Math.max(maxCoveredRows, coveredRowCount);
39 }
40
41 return maxCoveredRows;
42 }
43}
44
1class Solution {
2public:
3 int maximumRows(vector<vector<int>>& matrix, int numSelect) {
4 int rowCount = matrix.size();
5 int colCount = matrix[0].size();
6
7 // Convert each row to a bitmask where bit j is set if matrix[i][j] == 1
8 vector<int> rowBitmasks(rowCount, 0);
9 for (int i = 0; i < rowCount; ++i) {
10 for (int j = 0; j < colCount; ++j) {
11 if (matrix[i][j] == 1) {
12 rowBitmasks[i] |= (1 << j);
13 }
14 }
15 }
16
17 int maxCoveredRows = 0;
18
19 // Try all possible combinations of selecting numSelect columns
20 for (int columnMask = 1; columnMask < (1 << colCount); ++columnMask) {
21 // Skip if the number of selected columns is not equal to numSelect
22 if (__builtin_popcount(columnMask) != numSelect) {
23 continue;
24 }
25
26 // Count how many rows are fully covered by the selected columns
27 int coveredRowCount = 0;
28 for (int rowMask : rowBitmasks) {
29 // A row is covered if all its 1s are in the selected columns
30 // This means (rowMask & columnMask) should equal rowMask
31 if ((rowMask & columnMask) == rowMask) {
32 coveredRowCount++;
33 }
34 }
35
36 // Update the maximum number of covered rows
37 maxCoveredRows = max(maxCoveredRows, coveredRowCount);
38 }
39
40 return maxCoveredRows;
41 }
42};
43
1/**
2 * Finds the maximum number of rows that can be covered by selecting numSelect columns
3 * @param matrix - Binary matrix where 1s need to be covered
4 * @param numSelect - Number of columns to select
5 * @returns Maximum number of rows that can be fully covered
6 */
7function maximumRows(matrix: number[][], numSelect: number): number {
8 const rowCount: number = matrix.length;
9 const colCount: number = matrix[0].length;
10
11 // Convert each row to a bitmask representation
12 // If matrix[i][j] is 1, set the j-th bit in rowBitmasks[i]
13 const rowBitmasks: number[] = Array(rowCount).fill(0);
14 for (let row = 0; row < rowCount; row++) {
15 for (let col = 0; col < colCount; col++) {
16 if (matrix[row][col] === 1) {
17 rowBitmasks[row] |= 1 << col;
18 }
19 }
20 }
21
22 let maxCoveredRows: number = 0;
23
24 // Try all possible combinations of selecting numSelect columns
25 // Each mask represents a selection of columns (bit i = 1 means column i is selected)
26 for (let columnMask = 1; columnMask < (1 << colCount); columnMask++) {
27 // Skip if this mask doesn't select exactly numSelect columns
28 if (bitCount(columnMask) !== numSelect) {
29 continue;
30 }
31
32 // Count how many rows can be fully covered by this column selection
33 let coveredRowCount: number = 0;
34 for (const rowBitmask of rowBitmasks) {
35 // A row is covered if all its 1s are in selected columns
36 // This means rowBitmask AND columnMask should equal rowBitmask
37 if ((rowBitmask & columnMask) === rowBitmask) {
38 coveredRowCount++;
39 }
40 }
41
42 maxCoveredRows = Math.max(maxCoveredRows, coveredRowCount);
43 }
44
45 return maxCoveredRows;
46}
47
48/**
49 * Counts the number of set bits (1s) in a number using Brian Kernighan's algorithm
50 * @param i - Integer to count bits in
51 * @returns Number of set bits
52 */
53function bitCount(i: number): number {
54 // Subtract pairs of bits
55 i = i - ((i >>> 1) & 0x55555555);
56 // Add pairs of bits
57 i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
58 // Add groups of 4 bits
59 i = (i + (i >>> 4)) & 0x0f0f0f0f;
60 // Add groups of 8 bits
61 i = i + (i >>> 8);
62 // Add groups of 16 bits
63 i = i + (i >>> 16);
64 // Return the final count (max 32 bits, so mask with 0x3f)
65 return i & 0x3f;
66}
67
Time and Space Complexity
The time complexity is O(2^n × m)
, where m
is the number of rows and n
is the number of columns in the matrix.
Breaking down the time complexity:
- The first loop iterates through
m
rows, and for each row, it processesn
columns to create a bitmask, takingO(m × n)
time. - The second loop iterates through all possible column selections, which is
2^n
possibilities (since each column can either be selected or not). - For each of the
2^n
masks, we check if it has exactlynumSelect
bits set usingbit_count()
inO(1)
time. - When a valid mask is found, we iterate through all
m
row masks to count covered rows, takingO(m)
time. - Therefore, the dominant term is
O(2^n × m)
.
The space complexity is O(m)
, where m
is the number of rows in the matrix.
Breaking down the space complexity:
- The
rows
list storesm
integer bitmasks, one for each row, requiringO(m)
space. - The variables
mask
,ans
, andt
use constant spaceO(1)
. - No recursive calls or additional data structures that scale with input size are used.
- Therefore, the total space complexity is
O(m)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding Row Coverage for All-Zero Rows
A critical pitfall is incorrectly handling rows that contain only zeros. Many developers mistakenly think they need special logic to check for all-zero rows.
Incorrect Approach:
# Wrong: Adding unnecessary special case handling
covered = 0
for row, row_mask in zip(matrix, row_masks):
if all(val == 0 for val in row): # Unnecessary check
covered += 1
elif (row_mask & column_selection_mask) == row_mask:
covered += 1
Why This Happens: The problem statement explicitly mentions "The row contains no 1s at all (all cells are 0)" as a coverage condition, leading developers to think they need separate handling.
The Reality: When a row has all zeros, its bitmask is 0
. The condition (0 & column_selection_mask) == 0
is always True
regardless of which columns are selected, automatically handling this case.
Correct Approach:
# The existing code already handles all-zero rows correctly
covered_rows_count = sum(
(row_mask & column_selection_mask) == row_mask
for row_mask in row_masks
)
2. Off-by-One Error in Column Indexing
Another common mistake occurs when manually creating bitmasks without using enumerate properly, leading to incorrect bit positions.
Incorrect Approach:
# Wrong: Using 1-based indexing or incorrect bit shifting
row_mask = 0
for j in range(len(row)):
if row[j] == 1:
row_mask |= (1 << (j + 1)) # Wrong: shifts by j+1 instead of j
Correct Approach:
# Use enumerate to ensure correct 0-based indexing
row_mask = reduce(or_, (1 << col_idx for col_idx, value in enumerate(row) if value == 1), 0)
3. Integer Overflow Concerns (Language-Specific)
In languages with fixed-size integers, having too many columns could cause overflow issues. While Python handles arbitrary-precision integers automatically, this could be problematic in other languages.
Potential Issue in Other Languages:
- With 32 columns in C++ using
int
, the bitmask1 << 31
might cause undefined behavior - Solution: Use appropriate data types like
unsigned long long
oruint64_t
Python Advantage:
# Python handles large integers automatically
for column_selection_mask in range(1 << num_columns): # Works even if num_columns > 32
4. Inefficient Bit Counting
Using manual bit counting instead of built-in methods can lead to both performance issues and potential bugs.
Inefficient/Error-Prone Approach:
# Manual bit counting - slower and more error-prone
def count_bits(n):
count = 0
while n:
count += n & 1
n >>= 1
return count
if count_bits(column_selection_mask) != numSelect:
continue
Optimal Approach:
# Use built-in bit_count() method (Python 3.10+)
if column_selection_mask.bit_count() != numSelect:
continue
# For older Python versions, use bin().count('1')
if bin(column_selection_mask).count('1') != numSelect:
continue
These pitfalls highlight the importance of understanding how bitmask operations inherently handle edge cases and leveraging built-in language features for clarity and efficiency.
What's the output of running the following function using the following tree as input?
1def serialize(root):
2 res = []
3 def dfs(root):
4 if not root:
5 res.append('x')
6 return
7 res.append(root.val)
8 dfs(root.left)
9 dfs(root.right)
10 dfs(root)
11 return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4 StringJoiner res = new StringJoiner(" ");
5 serializeDFS(root, res);
6 return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10 if (root == null) {
11 result.add("x");
12 return;
13 }
14 result.add(Integer.toString(root.val));
15 serializeDFS(root.left, result);
16 serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2 let res = [];
3 serialize_dfs(root, res);
4 return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8 if (!root) {
9 res.push("x");
10 return;
11 }
12 res.push(root.val);
13 serialize_dfs(root.left, res);
14 serialize_dfs(root.right, res);
15}
16
Recommended Readings
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
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!