Facebook Pixel

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 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.

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 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.

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

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

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

Load More