2397. Maximum Rows Covered by Columns

MediumBit ManipulationArrayBacktrackingEnumerationMatrix
Leetcode Link

Problem Description

The problem provides us with a binary matrix where rows represent different items, and columns represent attributes that are either present (1) or absent (0). We are also given a number numSelect which indicates how many columns we can choose.

A row is said to be covered by the selection if for each "1" in the row, the corresponding column is part of the chosen columns, or if the row does not contain any "1"s. The goal is to select numSelect columns in such a way that the maximum number of rows are covered.

The challenge is to examine all possible combinations of columns that can be selected and then determine which combination covers the most rows.

Flowchart Walkthrough

Let's analyze LeetCode problem 2397. Maximum Rows Covered by Columns using our algorithm Flowchart. We'll follow the flowchart to determine the most appropriate algorithm for the problem:

  1. Is it a graph?

    • No: This problem does not deal with nodes connected by edges typical of graph problems.
  2. Need to solve for kth smallest/largest?

    • No: The problem does not involve finding a "kth" element.
  3. Involves Linked Lists?

    • No: It does not involve operations or data structures related to linked lists.
  4. Does the problem have small constraints?

    • Yes: The problem involves selecting columns, likely limited in number, to cover maximum rows - a scenario where the total potential combinations are typically suitable for exhaustive searching when constraints are small.
  5. Brute force / Backtracking?

    • Yes: Given the small constraints and the need to examine all combinations to find the optimal solution, backtracking provides a structured way to explore all possibilities, determining the selection of columns that maximizes the number of covered rows.

Conclusion: According to the Flowchart, using the Backtracking pattern is the suggested approach to solve this problem. It allows for a systematic exploration of all column combinations, finding the subset that covers the most rows.

Intuition

The intuition behind the solution is to use bit manipulation to represent the presence or absence of columns in a more computationally efficient manner. Each row of the matrix can be represented by a bitmask where the bit is set to 1 if the corresponding column contains a 1. Selecting numSelect columns is equivalent to creating a bitmask with numSelect number of 1s, representing the columns being selected.

The problem is then reduced to iterating over all possible combinations of selected columns (which are represented by bitmasks with exactly numSelect 1s). For each combination, we determine how many rows are covered by using a logical "AND" operation. A row is covered if, after the "AND" operation between the row's bitmask and the chosen columns' bitmask, the result is equal to the row's bitmask.

To count the number of 1s in a bitmask (or check if a bitmask has exactly numSelect 1s), the bit_count() function in Python is utilized. The max function is then used to keep track of the maximum number of rows that have been covered so far.

Through this approach using bitmasks and iteration, we can find the optimal selection of columns without explicitly checking each element of the subset in the original matrix, saving both time and space, and providing an optimized solution to the problem.

Learn more about Backtracking patterns.

Solution Approach

The solution approach relies on bit manipulation and enumeration. Let's walk through the implementation as provided in the reference solution with particular emphasis on the algorithms, data structures, and patterns used:

  1. We start by representing each row as a bitmask. This is done by iterating over each row in the original matrix and for every "1" encountered, a corresponding bit in the mask is set. This uses list comprehension along with the reduce function from functools and the or_ bitwise operator from operator.

    rows = []
    for row in matrix:
        mask = reduce(or_, (1 << j for j, x in enumerate(row) if x), 0)
        rows.append(mask)
  2. Next, we want to enumerate all possible combinations of columns we can choose. As each column can be represented by a bit in a bitmask, we iterate over the range 0 to 2^n (where n is the number of columns), using a for loop. Each number in this range corresponds to a potential combination of columns, with the bit at position j representing column j.

    for mask in range(1 << len(matrix[0])):
        if mask.bit_count() != numSelect:
            continue
        # ... Check coverage for this combination
  3. Within this loop, we use the bit_count method on the bitmask to check whether the number of columns chosen (numSelect) is equal to the number of 1s in the bitmask. If not, we skip to the next iteration:

    if mask.bit_count() != numSelect:
        continue
  4. For each valid combination of selected columns, we check how many rows are covered. This is done with a neat one-liner list comprehension by iterating over each row's bitmask and checking if the row's bitmask AND the selected column bitmask equals the row's bitmask, meaning the row is covered:

    t = sum((x & mask) == x for x in rows)
  5. Finally, we update our answer with the maximum value between the current answer and the number of covered rows for this column selection:

    ans = max(ans, t)
  6. Return the maximum number of rows covered after examining all combinations:

    return ans

The overall pattern is an exhaustive search where we iterate over all possible selections of columns and, for each selection, we count how many rows are covered. This brute-force approach is made tractable by using bitmasks to efficiently represent and compare sets of columns and rows.

By compactly representing columns and rows as bits, we avoid dealing with actual column elements and row elements directly, which reduces computational complexity. However, since the number of possible combinations is 2^n, this approach is still exponential in time complexity, but it's practical for small input sizes where n (the number of columns) is not too large.

The key data structures here:

  • A list rows that holds the bitmask representation of each row.
  • An integer mask that holds the bitmask representation of each possible column selection.
  • An integer ans that keeps track of the maximum number of rows that can be covered.

Ready to land your dream job?

Unlock your dream job with a 2-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's illustrate the solution approach with a small example. Consider a binary matrix matrix and numSelect as follows:

matrix = [
    [1, 0, 0],
    [0, 1, 1],
    [1, 1, 0]
]
numSelect = 2

Our task is to choose 2 columns that cover the maximum number of rows based on the given rules.

  1. First, we convert each row of the matrix to a bitmask representation, where each "1" present in the row corresponds to a set bit in the bitmask:

    • Row 1 bitmask = 100 (in binary) = 4 (in decimal)
    • Row 2 bitmask = 011 (in binary) = 3 (in decimal)
    • Row 3 bitmask = 110 (in binary) = 6 (in decimal)
  2. The list of bitmasks corresponding to the rows will be rows = [4, 3, 6].

  3. Next, we need to generate all possible combinations of the columns that we can select and check if they cover the rows. Since numSelect = 2, we are looking for bitmasks with exactly 2 set bits. Our matrix has 3 columns, so we iterate over the range 0 to 2^3 or 0 to 8 in decimal.

  4. The possible column combinations with 2 set bits are 110 (6 in decimal), 101 (5 in decimal) and 011 (3 in decimal). These correspond to selecting columns [1, 2], [1, 3], and [2, 3], respectively.

  5. Now, check each valid column bitmask against all the row bitmasks. For each combination:

    • Consider mask = 6 (selecting columns [1, 2]):

      • Row 1 (4): (4 & 6) == 4 is True (covered)
      • Row 2 (3): (3 & 6) == 3 is True (covered)
      • Row 3 (6): (6 & 6) == 6 is True (covered)

      This selection covers all 3 rows.

    • Consider mask = 5 (selecting columns [1, 3]):

      • Row 1 (4): (4 & 5) == 4 is True (covered)
      • Row 2 (3): (3 & 5) == 1 is False (not covered)
      • Row 3 (6): (6 & 5) == 4 is False (not covered)

      This selection covers 1 row.

    • Consider mask = 3 (selecting columns [2, 3]):

      • Row 1 (4): (4 & 3) == 0 is False (not covered)
      • Row 2 (3): (3 & 3) == 3 is True (covered)
      • Row 3 (6): (6 & 3) == 2 is False (not covered)

      This selection covers 1 row.

  6. The maximum number of rows covered by any selection is 3 (from the bitmask 6, which represents selecting columns [1, 2]).

  7. Therefore, the output for this example would be 3 as it is the maximum number of rows that can be covered by selecting 2 columns.

Solution Implementation

1from functools import reduce
2from operator import or_
3
4class Solution:
5    def maximumRows(self, matrix, num_select):
6        # Convert each row of the matrix to a bitmask where 1's represent columns
7        # that have the value 1. This will allow us to easily compare which rows
8        # can be entirely covered by selecting certain columns.
9        row_masks = [reduce(or_, (1 << j for j, cell in enumerate(row) if cell), 0) for row in matrix]
10
11        max_covered_rows = 0  # Initialize the max number of rows that can be covered
12      
13        # Iterate over all possible selections of columns as bitmasks.
14        for col_mask in range(1 << len(matrix[0])):
15            # Check if the number of selected columns matches the required `num_select`
16            if bin(col_mask).count('1') != num_select:
17                continue
18          
19            # Count the number of rows that can be entirely covered by the selected columns
20            covered_row_count = sum(1 for row_mask in row_masks if (row_mask & col_mask) == row_mask)
21          
22            # Update the maximum number of rows that can be covered
23            max_covered_rows = max(max_covered_rows, covered_row_count)
24          
25        return max_covered_rows
26
1class Solution {
2    public int maximumRows(int[][] matrix, int numSelect) {
3        int rowCount = matrix.length; // total number of rows in the matrix
4        int colCount = matrix[0].length; // total number of columns in the matrix
5        int[] rowBitmasks = new int[rowCount]; // array to store the bitmask representation of each row
6
7        // Convert every row in the matrix to their respective bitmask representation
8        for (int rowIndex = 0; rowIndex < rowCount; ++rowIndex) {
9            for (int colIndex = 0; colIndex < colCount; ++colIndex) {
10                // If the cell contains a 1, update the bitmask for that row
11                if (matrix[rowIndex][colIndex] == 1) {
12                    rowBitmasks[rowIndex] |= 1 << colIndex;
13                }
14            }
15        }
16
17        int maxRowsCompleted = 0; // variable to store the maximum number of rows that can be completed
18
19        // Iterate over all possible combinations of columns to select
20        for (int mask = 1; mask < 1 << colCount; ++mask) {
21            // Continue only if the bit count of 'mask' equals 'numSelect'
22            if (Integer.bitCount(mask) != numSelect) {
23                continue;
24            }
25          
26            int completedRows = 0; // variable to count how many rows are "completed" with the current mask
27
28            // Check each row to see if the selected columns can complete the row
29            for (int rowBitmask : rowBitmasks) {
30                // A row is considered complete if the columns selected by 'mask' cover all the 1s in the row
31                if ((rowBitmask & mask) == rowBitmask) {
32                    completedRows++; // increment the number of completed rows
33                }
34            }
35
36            // Update the maximum number of rows that we can complete with the current combination
37            maxRowsCompleted = Math.max(maxRowsCompleted, completedRows);
38        }
39      
40        return maxRowsCompleted; // return the maximum number of rows that can be completed
41    }
42}
43
1#include <vector>
2#include <algorithm>
3#include <cstring>
4
5class Solution {
6public:
7    // Returns the maximum number of rows where all '1's are covered when choosing 'numSelect' columns
8    int maximumRows(vector<vector<int>>& matrix, int numSelect) {
9        int rowCount = matrix.size(); // Number of rows in the matrix
10        int colCount = matrix[0].size(); // Number of columns in the matrix
11        vector<int> rowMasks(rowCount, 0); // Vector to store the bitmask representation of each row
12      
13        // Convert each row of the matrix into a bitmask and store it in rowMasks
14        for (int i = 0; i < rowCount; ++i) {
15            for (int j = 0; j < colCount; ++j) {
16                if (matrix[i][j]) {
17                    rowMasks[i] |= 1 << j;
18                }
19            }
20        }
21
22        int maxRowsCovered = 0; // Variable to keep track of the maximum rows covered
23      
24        // Iterate through all possible combinations of selected columns
25        for (int mask = 1; mask < (1 << colCount); ++mask) {
26            // If the number of selected columns doesn't match 'numSelect', skip this combination
27            if (__builtin_popcount(mask) != numSelect) {
28                continue;
29            }
30
31            int currentCovered = 0; // Counter for the number of rows fully covered in this combination
32          
33            // Check each row to see if it is fully covered by the selected columns
34            for (int rowMask : rowMasks) {
35                // If the intersection of the row bitmask and selected columns equals the row bitmask,
36                // it means all '1's in that row are covered
37                if ((rowMask & mask) == rowMask) {
38                    currentCovered++;
39                }
40            }
41
42            // Update the maximum rows covered if the current configuration covers more rows
43            maxRowsCovered = max(maxRowsCovered, currentCovered);
44        }
45
46        // Return the maximum number of rows that can be covered
47        return maxRowsCovered;
48    }
49};
50
1function maximumRows(matrix: number[][], numSelect: number): number {
2    const rowCount = matrix.length; // Number of rows in the matrix
3    const colCount = matrix[0].length; // Number of columns in the matrix
4    const rowMasks: number[] = new Array(rowCount).fill(0); // Array to store the bitmask representation of each row
5  
6    // Convert each row of the matrix into a bitmask and store it in rowMasks
7    for (let i = 0; i < rowCount; ++i) {
8        for (let j = 0; j < colCount; ++j) {
9            if (matrix[i][j]) {
10                rowMasks[i] |= 1 << j;
11            }
12        }
13    }
14
15    let maxRowsCovered = 0; // Variable to keep track of the maximum rows covered
16
17    // Iterate through all possible combinations of selected columns
18    for (let mask = 1; mask < (1 << colCount); ++mask) {
19        // If the number of selected columns doesn't match 'numSelect', skip this combination
20        if (popCount(mask) !== numSelect) {
21            continue;
22        }
23
24        let currentCovered = 0; // Counter for the number of rows fully covered in this combination
25
26        // Check each row to see if it is fully covered by the selected columns
27        for (let rowMask of rowMasks) {
28            // If the intersection between the row bitmask and selected columns equals the row bitmask,
29            // it means all '1's in that row are covered
30            if ((rowMask & mask) === rowMask) {
31                currentCovered++;
32            }
33        }
34
35        // Update the maximum rows covered if the current configuration covers more rows
36        maxRowsCovered = Math.max(maxRowsCovered, currentCovered);
37    }
38
39    // Return the maximum number of rows that can be covered
40    return maxRowsCovered;
41}
42
43// Helper function to count the number of '1's in binary representation of a number
44function popCount(n: number): number {
45    let count = 0;
46    while (n) {
47        count += n & 1;
48        n >>>= 1;
49    }
50    return count;
51}
52

Time and Space Complexity

The time complexity of the given code snippet primarily arises from the nested loops. The number of columns in the matrix is denoted as C = len(matrix[0]) and the number of rows is R = len(matrix). The for loop through every row has a complexity of O(R), and within that loop, it iterates through all columns, incurring a complexity of O(C) resulting in a total of O(R * C) for this segment.

Additionally, the code iterates through every possible combination of columns (1 << C possibilities), with a complexity of O(2^C). Within that loop, for every mask, we iterate over all the rows to perform bitmask comparisons, resulting in an O(R) complexity for this part.

Combining these, the overall time complexity is O(R * C + R * 2^C), which simplifies to O(R * (C + 2^C)).

The space complexity of this code is O(R). This arises due to the additional rows list storing a mask for each row. Each mask is an integer and there are R masks to store. The other variables used in the code are of constant size and therefore do not significantly add to the complexity.

Learn more about how to find time and space complexity quickly using problem constraints.


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?


Recommended Readings

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