Facebook Pixel

1072. Flip Columns For Maximum Number of Equal Rows

MediumArrayHash TableMatrix
Leetcode Link

Problem Description

You have a binary matrix (containing only 0s and 1s) with m rows and n columns. You can perform an operation where you select any number of columns and flip all cells in those columns. Flipping means changing 0 to 1 and 1 to 0.

Your goal is to find the maximum number of rows that can have all their values equal (either all 0s or all 1s) after performing some column flips.

For example, if you have a row [0, 1, 0] and you flip column 2, it becomes [0, 0, 0] - all values are now equal.

The key insight is that two rows can both become uniform (all same values) after the same set of column flips if and only if they are either identical or completely opposite to each other. This is because:

  • If two rows are identical, flipping any columns affects them the same way
  • If two rows are completely opposite (like [0, 1, 0] and [1, 0, 1]), they can both become uniform with the same flips - one becomes all 0s while the other becomes all 1s

The solution normalizes each row by checking if it starts with 0. If it does, keep it as is; if it starts with 1, flip all values. This groups together rows that can be made uniform with the same column flips. The algorithm then counts occurrences of each normalized pattern and returns the maximum count, which represents the maximum number of rows that can be made uniform simultaneously.

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

Intuition

The key observation is understanding when two rows can both be made uniform (all 0s or all 1s) using the same set of column flips.

Let's think about what happens when we flip columns. If we have a row like [0, 1, 0] and want to make it all 0s, we need to flip column 2. After this flip, any other row in the matrix is also affected by this same column flip.

Now consider two scenarios:

  1. If another row is [0, 1, 0] (identical to the first), flipping column 2 also makes it [0, 0, 0]
  2. If another row is [1, 0, 1] (completely opposite), flipping column 2 makes it [1, 1, 1]

This reveals the pattern: two rows can both become uniform with the same column flips if and only if they are either identical or complete opposites of each other.

Why? Because when we choose columns to flip to make one row uniform:

  • Identical rows will behave exactly the same way and become the same uniform value
  • Opposite rows will become the opposite uniform value (if one becomes all 0s, the other becomes all 1s)

This leads to the solution strategy: we need to group rows that are either identical or opposites together, as they can be made uniform simultaneously. To simplify this grouping, we can normalize all rows by a consistent rule - if a row starts with 1, we flip all its values to start with 0. This way, rows that were originally opposite will become identical after normalization, making them easy to count together.

The maximum count among all such groups gives us the answer - the maximum number of rows that can be made uniform with a single choice of column flips.

Solution Approach

The implementation uses a hash map (Counter in Python) to group and count rows that can be made uniform together.

Here's the step-by-step approach:

  1. Initialize a Counter: Create a Counter object to store the frequency of each normalized row pattern.

  2. Process each row: For each row in the matrix:

    • Normalize the row: Check if the first element is 0
      • If row[0] == 0: Keep the row as is and convert it to a tuple tuple(row)
      • If row[0] == 1: Flip all bits in the row using XOR operation tuple(x ^ 1 for x in row)
    • This normalization ensures that rows which are complete opposites will have the same tuple representation after processing
  3. Count occurrences: Add the normalized tuple to the counter. The counter automatically tracks how many times each pattern appears.

  4. Return the maximum: Use max(cnt.values()) to find the largest group of rows that can be made uniform together.

The XOR operation (x ^ 1) is used to flip bits efficiently:

  • 0 ^ 1 = 1 (flips 0 to 1)
  • 1 ^ 1 = 0 (flips 1 to 0)

By using tuples as keys in the counter, we can compare entire row patterns efficiently. Tuples are immutable and hashable, making them perfect for dictionary keys.

The time complexity is O(m × n) where m is the number of rows and n is the number of columns, as we process each element once. The space complexity is O(m × n) in the worst case when all rows are unique after normalization.

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 work through a small example with the matrix:

[0, 0, 1]
[1, 1, 0]
[0, 0, 1]
[0, 1, 1]

Step 1: Process Row 1 [0, 0, 1]

  • First element is 0, so keep as is
  • Normalized form: (0, 0, 1)
  • Counter: {(0, 0, 1): 1}

Step 2: Process Row 2 [1, 1, 0]

  • First element is 1, so flip all bits using XOR
  • 1^1=0, 1^1=0, 0^1=1
  • Normalized form: (0, 0, 1)
  • Counter: {(0, 0, 1): 2}

Notice that [1, 1, 0] and [0, 0, 1] are opposites, so they normalize to the same pattern!

Step 3: Process Row 3 [0, 0, 1]

  • First element is 0, so keep as is
  • Normalized form: (0, 0, 1)
  • Counter: {(0, 0, 1): 3}

Step 4: Process Row 4 [0, 1, 1]

  • First element is 0, so keep as is
  • Normalized form: (0, 1, 1)
  • Counter: {(0, 0, 1): 3, (0, 1, 1): 1}

Step 5: Find Maximum

  • Maximum count is 3

Verification: If we flip columns 1 and 2:

  • Row 1 [0, 0, 1][1, 1, 0] (all different bits flipped)
  • Row 2 [1, 1, 0][0, 0, 1] (all different bits flipped)
  • Row 3 [0, 0, 1][1, 1, 0] (all different bits flipped)

Wait, let me recalculate the flips. If we flip only column 3:

  • Row 1 [0, 0, 1][0, 0, 0] (all 0s) ✓
  • Row 2 [1, 1, 0][1, 1, 1] (all 1s) ✓
  • Row 3 [0, 0, 1][0, 0, 0] (all 0s) ✓
  • Row 4 [0, 1, 1][0, 1, 0] (not uniform) ✗

Indeed, 3 rows can be made uniform by flipping column 3, which matches our answer!

Solution Implementation

1from typing import List
2from collections import Counter
3
4class Solution:
5    def maxEqualRowsAfterFlips(self, matrix: List[List[int]]) -> int:
6        # Counter to track frequency of normalized row patterns
7        pattern_counter = Counter()
8      
9        # Process each row in the matrix
10        for row in matrix:
11            # Normalize the row pattern:
12            # If the row starts with 0, keep it as is
13            # If the row starts with 1, flip all bits (XOR with 1)
14            # This normalization helps identify rows that can become equal after column flips
15            if row[0] == 0:
16                normalized_pattern = tuple(row)
17            else:
18                # Flip all bits in the row (0->1, 1->0)
19                normalized_pattern = tuple(bit ^ 1 for bit in row)
20          
21            # Count occurrences of this normalized pattern
22            pattern_counter[normalized_pattern] += 1
23      
24        # Return the maximum count among all patterns
25        # This represents the maximum number of rows that can be made equal
26        return max(pattern_counter.values())
27
1class Solution {
2    public int maxEqualRowsAfterFlips(int[][] matrix) {
3        // Map to store pattern frequency: pattern -> count
4        Map<String, Integer> patternCount = new HashMap<>();
5      
6        // Initialize result and get number of columns
7        int maxRows = 0;
8        int numColumns = matrix[0].length;
9      
10        // Process each row in the matrix
11        for (int[] currentRow : matrix) {
12            // Create a character array to store the pattern
13            // Pattern represents relative differences from the first element
14            char[] pattern = new char[numColumns];
15          
16            // Generate pattern by XORing each element with the first element
17            // If element equals first element, result is 0; otherwise 1
18            // This identifies rows that can become equal after flipping
19            for (int col = 0; col < numColumns; col++) {
20                pattern[col] = (char) (currentRow[0] ^ currentRow[col]);
21            }
22          
23            // Convert pattern to string and update its count in the map
24            // merge() increments the count if key exists, or sets to 1 if new
25            String patternKey = String.valueOf(pattern);
26            int updatedCount = patternCount.merge(patternKey, 1, Integer::sum);
27          
28            // Track the maximum count seen so far
29            maxRows = Math.max(maxRows, updatedCount);
30        }
31      
32        // Return the maximum number of rows that can be made equal
33        return maxRows;
34    }
35}
36
1class Solution {
2public:
3    int maxEqualRowsAfterFlips(vector<vector<int>>& matrix) {
4        // Map to store normalized pattern of each row and its frequency
5        unordered_map<string, int> patternFrequency;
6        int maxRows = 0;
7      
8        // Iterate through each row in the matrix
9        for (auto& row : matrix) {
10            string pattern;
11          
12            // Normalize the row pattern:
13            // If first element is 0, keep the row as is
14            // If first element is 1, flip all bits (XOR with 1)
15            // This ensures rows that can become equal after flips have the same pattern
16            for (int value : row) {
17                if (row[0] == 0) {
18                    // Keep original value when first element is 0
19                    pattern.push_back('0' + value);
20                } else {
21                    // Flip the bit when first element is 1
22                    pattern.push_back('0' + (value ^ 1));
23                }
24            }
25          
26            // Increment frequency count for this pattern and update maximum
27            patternFrequency[pattern]++;
28            maxRows = max(maxRows, patternFrequency[pattern]);
29        }
30      
31        return maxRows;
32    }
33};
34
1/**
2 * Finds the maximum number of rows that can be made equal after flipping columns
3 * @param matrix - 2D binary matrix containing only 0s and 1s
4 * @returns Maximum number of rows that can be made equal
5 */
6function maxEqualRowsAfterFlips(matrix: number[][]): number {
7    // Map to count frequency of normalized row patterns
8    const patternFrequency = new Map<string, number>();
9  
10    // Track the maximum frequency found
11    let maxEqualRows = 0;
12  
13    // Process each row in the matrix
14    for (const row of matrix) {
15        // Normalize the row: if first element is 1, flip all elements
16        // This ensures rows that can be made equal have the same pattern
17        if (row[0] === 1) {
18            for (let columnIndex = 0; columnIndex < row.length; columnIndex++) {
19                // XOR with 1 to flip: 0 becomes 1, 1 becomes 0
20                row[columnIndex] ^= 1;
21            }
22        }
23      
24        // Convert normalized row to string for use as map key
25        const rowPattern = row.join('');
26      
27        // Update frequency count for this pattern
28        const currentFrequency = (patternFrequency.get(rowPattern) || 0) + 1;
29        patternFrequency.set(rowPattern, currentFrequency);
30      
31        // Update maximum if current frequency is higher
32        maxEqualRows = Math.max(maxEqualRows, currentFrequency);
33    }
34  
35    return maxEqualRows;
36}
37

Time and Space Complexity

Time Complexity: O(m * n) where m is the number of rows and n is the number of columns in the matrix.

  • The outer loop iterates through all m rows of the matrix.
  • For each row, we perform the following operations:
    • Check if row[0] == 0: O(1)
    • If row[0] == 0, create a tuple from the row: O(n)
    • If row[0] != 0, apply XOR operation to each element and create a tuple: O(n)
    • Update the counter with the tuple as key: O(n) for hashing the tuple
  • Finding the maximum value in the counter: O(m) in the worst case when all rows are unique

Overall: O(m * n) + O(m) = O(m * n)

Space Complexity: O(m * n) in the worst case.

  • The Counter object cnt stores tuples as keys
  • In the worst case, all rows (after normalization) are unique, resulting in m different tuple keys
  • Each tuple has length n, so each key requires O(n) space
  • Total space for the counter: O(m * n)
  • Additional space for creating temporary tuples: O(n)

Overall: O(m * n) + O(n) = O(m * n)

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

Common Pitfalls

1. Using Lists as Dictionary Keys

A common mistake is trying to use lists directly as keys in the counter/dictionary:

# WRONG - Lists are mutable and unhashable
pattern_counter[row] += 1  # This will raise TypeError

Solution: Always convert lists to tuples before using them as dictionary keys:

# CORRECT - Tuples are immutable and hashable
pattern_counter[tuple(row)] += 1

2. Incorrect Normalization Logic

Some might normalize based on the majority of 0s or 1s in the row, or use more complex logic:

# WRONG - Normalizing based on count of 0s vs 1s
if row.count(0) > row.count(1):
    normalized = tuple(row)
else:
    normalized = tuple(bit ^ 1 for bit in row)

Solution: Always normalize based on the first element only. This ensures that identical rows and their complements map to the same pattern:

# CORRECT - Normalize based on first element
if row[0] == 0:
    normalized = tuple(row)
else:
    normalized = tuple(bit ^ 1 for bit in row)

3. Handling Empty Matrix

Failing to handle edge cases like empty matrices:

# WRONG - Will crash on empty matrix
return max(pattern_counter.values())  # ValueError if counter is empty

Solution: Add a check for empty matrices:

# CORRECT - Handle empty matrix
if not matrix or not matrix[0]:
    return 0
return max(pattern_counter.values()) if pattern_counter else 0

4. Misunderstanding the XOR Operation

Using incorrect bit flipping operations:

# WRONG - Using negation instead of XOR
normalized = tuple(not bit for bit in row)  # Returns booleans, not integers

# WRONG - Using subtraction
normalized = tuple(1 - bit for bit in row)  # Works but less efficient

Solution: Use XOR with 1 for clean bit flipping:

# CORRECT - XOR with 1 flips bits efficiently
normalized = tuple(bit ^ 1 for bit in row)

5. Not Understanding Why the Algorithm Works

A conceptual pitfall is implementing without understanding that two rows can be made uniform together if and only if they are identical or complete opposites. This might lead to trying to compare all possible flip combinations, resulting in exponential time complexity.

Solution: Understand that the normalization step groups together all rows that can be made uniform with the same set of column flips, reducing the problem to a simple counting task.

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

Which of the following shows the order of node visit in a Breadth-first Search?


Recommended Readings

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

Load More