1072. Flip Columns For Maximum Number of Equal Rows

MediumArrayHash TableMatrix
Leetcode Link

Problem Description

In this problem, we are provided with a binary matrix, meaning that each cell in the matrix contains either 0 or 1. Our goal is to maximize the number of rows that are all made up of the same value, either all 0s or all 1s, by performing a certain operation. The operation allowed is flipping the values in a column; flipping transforms all 0s to 1s and all 1s to 0s in that particular column.

To visualize, imagine you have a matrix like this:

11 0 0
21 1 0
30 1 1

You can flip, for instance, the second and third columns to get:

11 1 1
21 0 1
30 0 0

Thus, you would have 2 rows with all values equal.

The problem asks us to find the maximum number of such rows possible after performing any number of flips.

Intuition

The intuition behind the solution is based on an observation about flipping columns. When we flip columns in a matrix, we don't actually care about the original values but rather the pattern of the rows, because any row pattern (i.e., sequence of zeros and ones) can be flipped to its inverse. Thus, two rows that are inverses (complements) of each other will become identical after flipping the same set of columns.

Therefore, the key insight is that we can pair rows that are identical or are inverses of each other because they can be made equal with the flip operation. If we represent a row by a tuple, we can ensure that if the first element is 0, we keep it as is, and if the first element is 1, we invert the entire row. We do this because the first element determines the flip pattern we should use: if it's 0, no flip is needed for the first column, but if it's 1, we should flip the first column.

The approach uses a Counter to keep track of how many identical or invertible row patterns there are. It iterates through each row in the matrix, treating it directly if it starts with a 0 or treating the inverted row (flipping all bits) if it starts with a 1. The Counter will then have counts of how many times a certain pattern (or its inverse) appears in the matrix. The maximum of these counts is the number of rows that can be made equal by flipping the specified columns.

In summary, we rely on patterns and their complements to determine how many rows can be made equal through column flips by mapping each row to a unified pattern representation and counting the occurrences of these patterns.

Not Sure What to Study? Take the 2-min Quiz to Find Your Missing Piece:

What is the running time of the following code?

1int sqrt(int n) {
2  for (int guess = 1; guess * guess <= n; guess++) {
3    if (guess * guess == n) {
4      return guess;
5    }
6  }
7  return -1;
8}

Solution Approach

The implementation utilizes a Counter from the collections module in Python, which is a subclass of dict designed to count hashable objects. It implicitly counts how often each key is encountered. In the context of this problem, each key in the Counter will be a pattern (tuple) representing a row in the matrix after potentially inverting it.

Here is a breakdown of the steps involved in the implementation:

  1. Initialization:

    • Create a new Counter object named cnt.
  2. Processing Rows:

    • Iterate over each row in the input matrix.
    • For each row, check the first element:
      • If it is 0, use the row as is.
      • If it is 1, create a new tuple by flipping each element. The bitwise XOR operator ^ is used to flip bits - x ^ 1 changes 0 to 1 and 1 to 0.
  3. Counting Patterns:

    • Convert the row (either used directly or inverted) to a tuple, which acts as an immutable and hashable object suitable for use as a key in the Counter.
    • Increment the count of this tuple in cnt. This step essentially counts how many times we have seen this particular pattern or its complement in the matrix.
  4. Finding the Maximum Count:

    • After all rows are processed and counted, determine the maximum count using the max function on the values() of the Counter.
    • This maximum count represents the largest number of rows that can be made equal by flipping columns.

In essence, by using a Counter to tally row patterns, we are mapping the problem onto a frequency-count problem. Instead of directly manipulating the matrix, which would be costly, we represent each row by a pattern that considers flips and then look for the most common pattern. This drastically simplify the problem and allows us to find the solution efficiently.

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

Which data structure is used in a depth first search?

Example Walkthrough

Let's walk through the solution approach using a small example of a binary matrix:

1Matrix:
20 1 0
31 1 0
41 0 1

Following the solution approach step by step:

  1. Initialization:

    • We create an empty Counter object named cnt.
  2. Processing Rows:

    • We start by iterating over each row in the input matrix.

    • First row: 0 1 0

      • The first element is 0, so we use the row as is and move to step 3.
    • Second row: 1 1 0

      • The first element is 1, so we flip the entire row using the XOR operator: 0 0 1.
    • Third row: 1 0 1

      • The first element is 1, so we flip the entire row using the XOR operator: 0 1 0.
  3. Counting Patterns:

    • We convert each row into a tuple for counting.

    • First row becomes the tuple (0, 1, 0).

      • Increment cnt[(0, 1, 0)] by 1.
    • Second row (flipped) becomes the tuple (0, 0, 1).

      • Increment cnt[(0, 0, 1)] by 1.
    • Third row (flipped) becomes the tuple (0, 1, 0) (same as the first row).

      • Increment cnt[(0, 1, 0)] by 1 again. Now cnt[(0, 1, 0)] is 2.
    • Our Counter now has two keys with counts: [(0, 1, 0): 2, (0, 0, 1): 1].

  4. Finding the Maximum Count:

    • We find the maximum count in cnt, which in this case is 2 corresponding to the pattern (0, 1, 0).

Following these steps, we determine that the maximum number of rows with all values equal after flipping any number of columns is 2. In this case, if we flip the first and the last columns of the original matrix, we can make the first and third rows identical (all zeros), achieving our goal.

Solution Implementation

1from collections import Counter
2
3class Solution:
4    def maxEqualRowsAfterFlips(self, matrix: List[List[int]]) -> int:
5        # Initializing a counter to keep track of the frequency of each standardized row
6        row_counter = Counter()
7      
8        # Iterating through each row in the matrix
9        for row in matrix:
10            # Standardizing the row:
11            # If the first element is 0, keep the row unchanged; otherwise flip all bits
12            standardized_row = tuple(row) if row[0] == 0 else tuple(1 - x for x in row)
13          
14            # Updating the counter for the standardized row
15            row_counter[standardized_row] += 1
16      
17        # Returning the maximum frequency found in the counter as the result
18        return max(row_counter.values())
19
1class Solution {
2    public int maxEqualRowsAfterFlips(int[][] matrix) {
3        // HashMap to keep track of the frequency of each unique pattern
4        Map<String, Integer> patternFrequency = new HashMap<>();
5        // Variable to keep track of the max number of equal rows after flips
6        int maxEqualRows = 0;
7        // The number of columns in the matrix
8        int columnCount = matrix[0].length;
9
10        // Iterate over each row in the matrix
11        for (int[] row : matrix) {
12            // Create a character array to represent the pattern
13            char[] patternChars = new char[columnCount];
14            // Build the pattern based on the first element in the row
15            for (int i = 0; i < columnCount; ++i) {
16                // XOR the first element with each element in the row
17                // If row[0] == row[i], the result will be 0; otherwise, it will be 1
18                patternChars[i] = (char) ('0' + (row[0] ^ row[i]));
19            }
20            // Convert the character array to a string representing the pattern
21            String pattern = String.valueOf(patternChars);
22            // Update the pattern frequency map with the new pattern, 
23            // incrementing the count of the pattern by 1
24            int patternCount = patternFrequency.merge(pattern, 1, Integer::sum);
25            // Update maxEqualRows if the current pattern count is greater
26            maxEqualRows = Math.max(maxEqualRows, patternCount);
27        }
28
29        // Return the maximum number of equal rows that can be obtained after flips
30        return maxEqualRows;
31    }
32}
33
1#include <vector>
2#include <string>
3#include <unordered_map>
4#include <algorithm>
5using namespace std;
6
7class Solution {
8public:
9    int maxEqualRowsAfterFlips(vector<vector<int>>& matrix) {
10        unordered_map<string, int> patternCount; // This will map row patterns to their counts
11        int maxEqualRows = 0; // This will keep track of the maximum number of equal rows
12
13        // Iterate through rows of the matrix
14        for (auto& row : matrix) {
15            string pattern; // Initialize an empty string to store the row pattern
16
17            // Build the pattern for the given row considering flips
18            for (int cell : row) {
19                // If the first cell is 0, keep the number as is; otherwise flip the number
20                char representation = '0' + (row[0] == 0 ? cell : cell ^ 1);
21                pattern.push_back(representation);
22            }
23
24            // Increase the count for the current pattern
25            int currentCount = ++patternCount[pattern];
26            // Update maxEqualRows if the current pattern count exceeds it
27            maxEqualRows = max(maxEqualRows, currentCount);
28        }
29
30        return maxEqualRows; // Return the maximum number of equal rows after flips
31    }
32};
33
1/**
2 * Determines the maximum number of rows that can be made equal after a series of flips.
3 * Flips can be performed on an entire row which flips all 0s to 1s, and vice versa.
4 * @param {number[][]} matrix - The 2D array on which flips are performed.
5 * @returns {number} - The maximum number of rows that can be made equal by flipping.
6 */
7function maxEqualRowsAfterFlips(matrix: number[][]): number {
8    const countMap = new Map<string, number>(); // Map to store the frequency of each row pattern.
9    let maxEqualRows = 0; // Variable for tracking the maximum number of equal rows.
10
11    for (const row of matrix) {
12        // If the first element of the row is a 1, we flip the entire row to make sorting consistent.
13        if (row[0] === 1) {
14            // Perform the flip by XOR'ing each element in the row with 1.
15            for (let i = 0; i < row.length; i++) {
16                row[i] ^= 1;
17            }
18        }
19        // Convert the row to a string to use as a key in the map.
20        const rowString = row.join('');
21        // Update the count in the map for the given row pattern.
22        // If it doesn't exist yet, initialize to 0 then add 1, else increment the current count.
23        countMap.set(rowString, (countMap.get(rowString) || 0) + 1);
24        // Update maxEqualRows with the maximum of the current value and the new count for this pattern.
25        maxEqualRows = Math.max(maxEqualRows, countMap.get(rowString)!);
26    }
27
28    // Return the highest frequency of equal row patterns after flips.
29    return maxEqualRows;
30}
31
Not Sure What to Study? Take the 2-min Quiz:

Which of these pictures shows the visit order of a depth-first search?

Time and Space Complexity

Time Complexity

The time complexity of the given code is primarily determined by the number of iterations through each row of the matrix and operations performed for each row. Since we iterate once over each row, and for each row, we either take the tuple as-is if the starting element is 0 or iterate once more through the row to flip each bit and create a tuple, the time per row is O(N) where N is the number of columns. With M being the number of rows, iterating over all rows results in a time complexity of O(M * N).

Furthermore, the max function that is called on the Counter object takes O(U) time, where U is the number of unique rows after normalizing which in the worst case could be M. Thus, the overall time complexity combines the row iterations and the max function, and remains O(M * N).

Space Complexity

The space complexity depends on the space required to store the counter dictionary and the tuples created for each row. Each tuple can have at most N elements, and in the worst case, we could have M unique tuples if no two rows are the same or opposites. Therefore, the space complexity is O(M * N), since M tuples of N elements each may need to be stored in the counter dictionary.

In all, the space complexity of the algorithm is O(M * N).

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

Fast Track Your Learning with Our Quick Skills Quiz:

Given a sorted array of integers and an integer called target, find the element that equals to the target and return its index. Select the correct code that fills the ___ in the given code snippet.

1def binary_search(arr, target):
2    left, right = 0, len(arr) - 1
3    while left ___ right:
4        mid = (left + right) // 2
5        if arr[mid] == target:
6            return mid
7        if arr[mid] < target:
8            ___ = mid + 1
9        else:
10            ___ = mid - 1
11    return -1
12
1public static int binarySearch(int[] arr, int target) {
2    int left = 0;
3    int right = arr.length - 1;
4
5    while (left ___ right) {
6        int mid = left + (right - left) / 2;
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16
1function binarySearch(arr, target) {
2    let left = 0;
3    let right = arr.length - 1;
4
5    while (left ___ right) {
6        let mid = left + Math.trunc((right - left) / 2);
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16

Recommended Readings


Got a question? Ask the Teaching Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.


TA 👨‍🏫