2397. Maximum Rows Covered by Columns

MediumBit ManipulationArrayBacktrackingEnumerationMatrix
Leetcode Link

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 1s at all (all cells are 0)

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 1s (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 0s is automatically covered regardless of column selection
  • A row with 1s is only covered if ALL its 1s 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 from n total columns. The matrix dimensions are typically small enough that we can enumerate all possible combinations of columns. With n columns, there are C(n, numSelect) ways to choose columns, which is manageable for small values of n (typically ≤ 30).

Brute force / Backtracking?

  • Yes: Since we have small constraints and need to explore all possible combinations of selecting numSelect columns from n 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 1s 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 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

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: contribute 1 << 0 = 0001
  • Position 2 has 1: contribute 1 << 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 (binary 0101) means selecting columns 0 and 2
  • mask = 3 (binary 0011) 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 Evaluator

Example 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 (binary 011) → columns 0 and 1 selected ✓
  • mask = 5 (binary 101) → columns 0 and 2 selected ✓
  • mask = 6 (binary 110) → 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.

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 processes n columns to create a bitmask, taking O(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 exactly numSelect bits set using bit_count() in O(1) time.
  • When a valid mask is found, we iterate through all m row masks to count covered rows, taking O(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 stores m integer bitmasks, one for each row, requiring O(m) space.
  • The variables mask, ans, and t use constant space O(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 bitmask 1 << 31 might cause undefined behavior
  • Solution: Use appropriate data types like unsigned long long or uint64_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.

Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

In a binary min heap, the minimum element can be found in:


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More