Facebook Pixel

1787. Make the XOR of All Segments Equal to Zero

Problem Description

You are given an array nums and an integer k. The XOR of a segment [left, right] where left <= right is the XOR of all the elements with indices between left and right, inclusive: nums[left] XOR nums[left+1] XOR ... XOR nums[right].

Your task is to find the minimum number of elements to change in the array such that the XOR of all segments of size k is equal to zero.

In other words, after making the minimum number of changes to the array, every consecutive subarray of length k should have an XOR sum of 0. This means:

  • nums[0] XOR nums[1] XOR ... XOR nums[k-1] = 0
  • nums[1] XOR nums[2] XOR ... XOR nums[k] = 0
  • nums[2] XOR nums[3] XOR ... XOR nums[k+1] = 0
  • And so on for all possible segments of size k

The key insight is that if all segments of size k have XOR equal to 0, then the modified array must have a periodic pattern with period k. This is because for any index i, we have nums[i] = nums[i+k] (elements that are k positions apart must be equal).

The solution uses dynamic programming where elements are grouped by their position modulo k. Since elements at positions i, i+k, i+2k, etc. must all be the same value in the final array, we need to determine what value each group (position modulo k) should take to minimize the total number of changes while ensuring the XOR of the first k elements equals 0.

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

Intuition

Let's think about what the constraint "all segments of size k have XOR equal to 0" really means.

If we have two overlapping segments of size k:

  • Segment 1: nums[i] XOR nums[i+1] XOR ... XOR nums[i+k-1] = 0
  • Segment 2: nums[i+1] XOR nums[i+2] XOR ... XOR nums[i+k] = 0

From these two equations, we can derive that nums[i] = nums[i+k]. Why? Because if we XOR both equations together, all the overlapping elements cancel out (due to the property that x XOR x = 0), leaving us with nums[i] XOR nums[i+k] = 0, which means nums[i] = nums[i+k].

This reveals a crucial pattern: the array must be periodic with period k. Elements at positions 0, k, 2k, 3k, ... must all have the same value. Similarly, elements at positions 1, k+1, 2k+1, ... must have the same value, and so on.

So we can group elements by their position modulo k. Each group must ultimately contain only one unique value. The question becomes: what value should each group take?

There's one more constraint: the XOR of the first k elements (one element from each group) must equal 0. This means if we choose values v[0], v[1], ..., v[k-1] for each group, we need v[0] XOR v[1] XOR ... XOR v[k-1] = 0.

Now we need to minimize the total number of changes. For each group i, we count how many times each value appears in that group. If we choose value v for group i, the cost is size[i] - cnt[i][v] (we keep the elements that already have value v and change the rest).

This naturally leads to a dynamic programming approach: we process groups one by one, keeping track of all possible XOR values we can achieve with the groups processed so far. For each group, we consider:

  1. Changing all elements in the group to a completely new value (useful when no existing value in the group is beneficial)
  2. Changing elements to one of the existing values in the group (to minimize changes for that group)

The state f[j] represents the minimum cost to achieve XOR value j using the groups processed so far. When we process group i and want to achieve XOR value j after including this group, we look at what XOR value we need from previous groups (j XOR v where v is the value we choose for group i), and add the cost of setting group i to value v.

Learn more about Dynamic Programming patterns.

Solution Approach

The implementation follows the dynamic programming approach we identified:

Step 1: Group elements and count frequencies

First, we organize the array elements into k groups based on their position modulo k:

  • cnt[i] is a Counter that stores the frequency of each value in group i
  • size[i] stores the total number of elements in group i
cnt = [Counter() for _ in range(k)]
size = [0] * k
for i, v in enumerate(nums):
    cnt[i % k][v] += 1
    size[i % k] += 1

Step 2: Initialize DP array

We use f[j] to represent the minimum number of changes needed to achieve XOR value j with the groups processed so far. Since values in the array can be at most 2^10 - 1 (assuming reasonable constraints), we initialize:

n = 1 << 10  # 1024, covering all possible XOR values
f = [inf] * n
f[0] = 0  # Initially, XOR of 0 groups is 0 with 0 changes

Step 3: Process each group using DP

For each group i from 0 to k-1, we compute a new DP array g:

for i in range(k):
    g = [min(f) + size[i]] * n
    for j in range(n):
        for v, c in cnt[i].items():
            g[j] = min(g[j], f[j ^ v] + size[i] - c)
    f = g

Let's break down the transitions:

  1. Option 1 - Change all elements in the group:

    • g = [min(f) + size[i]] * n initializes all states with the cost of changing all elements in group i
    • We take the minimum cost from any previous XOR value (min(f)) and add size[i] (changing all elements)
  2. Option 2 - Use an existing value from the group:

    • For each possible target XOR value j and each existing value v in group i
    • If we set group i to value v, and we want final XOR to be j, then previous groups must have XOR of j ^ v
    • The cost is f[j ^ v] (previous cost) + size[i] - c (elements we need to change in this group)
    • Here, c = cnt[i][v] is the count of elements already having value v in group i

Step 4: Return the result

After processing all k groups, f[0] contains the minimum number of changes needed to make the XOR of all segments of size k equal to 0:

return f[0]

The key insight in the DP transition is using XOR properties: if we want the total XOR to be j and we're adding value v, the previous XOR must be j ^ v because (j ^ v) ^ v = j.

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 small example with nums = [1, 2, 0, 3, 0] and k = 3.

Step 1: Understanding the Goal We need all segments of size 3 to have XOR = 0:

  • Segment [0,1,2]: 1 XOR 2 XOR 0 = 3 (not 0)
  • Segment [1,2,3]: 2 XOR 0 XOR 3 = 1 (not 0)
  • Segment [2,3,4]: 0 XOR 3 XOR 0 = 3 (not 0)

Step 2: Grouping Elements Since the array must be periodic with period k=3, we group elements by position mod 3:

  • Group 0 (positions 0, 3): values are [1, 3]
  • Group 1 (positions 1, 4): values are [2, 0]
  • Group 2 (position 2): values are [0]

Each group must have all elements equal in the final array.

Step 3: Count Frequencies

  • cnt[0] = {1: 1, 3: 1}, size[0] = 2
  • cnt[1] = {2: 1, 0: 1}, size[1] = 2
  • cnt[2] = {0: 1}, size[2] = 1

Step 4: Dynamic Programming Initialize f[j] where f[j] = minimum changes to achieve XOR value j.

  • Initially: f[0] = 0, all others = infinity

Processing Group 0: For each possible XOR value j, we have two options:

  1. Change all elements in group 0 to a new value: cost = min(f) + size[0] = 0 + 2 = 2
  2. Use existing value 1 or 3 from group 0

For j = 1 (if we want XOR = 1 after group 0):

  • Use value 1: previous XOR needed = 1 XOR 1 = 0, cost = f[0] + (2-1) = 0 + 1 = 1
  • Use value 3: previous XOR needed = 1 XOR 3 = 2, cost = f[2] + (2-1) = inf
  • Result: g[1] = 1

For j = 3:

  • Use value 3: previous XOR needed = 3 XOR 3 = 0, cost = f[0] + (2-1) = 0 + 1 = 1
  • Result: g[3] = 1

After group 0: f[1] = 1, f[3] = 1, others = 2

Processing Group 1: For j = 3 (if we want XOR = 3 after groups 0,1):

  • Use value 2: previous XOR needed = 3 XOR 2 = 1, cost = f[1] + (2-1) = 1 + 1 = 2
  • Use value 0: previous XOR needed = 3 XOR 0 = 3, cost = f[3] + (2-1) = 1 + 1 = 2
  • Change all: cost = min(f) + 2 = 1 + 2 = 3
  • Result: g[3] = 2

After group 1: f[3] = 2, f[1] = 2, and other combinations

Processing Group 2: For j = 0 (final XOR must be 0):

  • Use value 0: previous XOR needed = 0 XOR 0 = 0, cost = f[0] + (1-0) = 2 + 0 = 2
  • But f[0] after group 1 is 2 (we need groups 0,1 to XOR to 0)

The minimum changes needed is 2.

Verification: One optimal solution: change nums[0] from 1 to 3, and nums[1] from 2 to 0. Result: [3, 0, 0, 3, 0]

  • Segment [0,1,2]: 3 XOR 0 XOR 0 = 3 XOR 0 = 3 XOR 0 = 3

Let me recalculate - actually we need: change nums[0] to 2 and nums[3] to 2. Result: [2, 2, 0, 2, 0]

  • All segments: 2 XOR 2 XOR 0 = 0

Total changes: 2

Solution Implementation

1class Solution:
2    def minChanges(self, nums: List[int], k: int) -> int:
3        # Maximum possible value (2^10 = 1024, covering all 10-bit numbers)
4        MAX_VAL = 1 << 10
5      
6        # For each position in the k-period cycle, store frequency of each value
7        # cnt[i] stores Counter of values at positions i, i+k, i+2k, ...
8        frequency_by_position = [Counter() for _ in range(k)]
9      
10        # Count total elements at each position in the k-period cycle
11        position_sizes = [0] * k
12      
13        # Build frequency maps for each position in the cycle
14        for index, value in enumerate(nums):
15            position_in_cycle = index % k
16            frequency_by_position[position_in_cycle][value] += 1
17            position_sizes[position_in_cycle] += 1
18      
19        # dp[xor_val] = minimum changes needed to achieve XOR value of xor_val
20        # using first i positions in the cycle
21        dp = [float('inf')] * MAX_VAL
22        dp[0] = 0  # Base case: 0 changes needed for XOR value 0 initially
23      
24        # Process each position in the k-period cycle
25        for position in range(k):
26            # next_dp[j] = minimum changes for XOR value j after processing current position
27            # Initialize with worst case: change all elements at current position
28            next_dp = [min(dp) + position_sizes[position]] * MAX_VAL
29          
30            # Try to optimize by keeping some values unchanged
31            for target_xor in range(MAX_VAL):
32                # For each value that appears at current position
33                for value, count in frequency_by_position[position].items():
34                    # If we want target_xor after this position and use 'value',
35                    # we need previous XOR to be (target_xor ^ value)
36                    prev_xor = target_xor ^ value
37                    # Cost: previous cost + (total at position - count we keep)
38                    changes_needed = dp[prev_xor] + position_sizes[position] - count
39                    next_dp[target_xor] = min(next_dp[target_xor], changes_needed)
40          
41            dp = next_dp
42      
43        # Return minimum changes needed to achieve XOR value of 0
44        return dp[0]
45
1class Solution {
2    public int minChanges(int[] nums, int k) {
3        // Maximum possible XOR value (2^10 = 1024, covering 0-1023)
4        final int MAX_XOR_VALUE = 1 << 10;
5      
6        // Store frequency of each number at each position modulo k
7        // frequencyMap[i] stores the frequency map for position i (mod k)
8        Map<Integer, Integer>[] frequencyMap = new Map[k];
9        Arrays.setAll(frequencyMap, i -> new HashMap<>());
10      
11        // Count total elements at each position modulo k
12        int[] groupSize = new int[k];
13      
14        // Build frequency maps for each group
15        for (int i = 0; i < nums.length; i++) {
16            int groupIndex = i % k;
17            frequencyMap[groupIndex].merge(nums[i], 1, Integer::sum);
18            groupSize[groupIndex]++;
19        }
20      
21        // dp[xorValue] = minimum changes needed to achieve xorValue
22        int[] dp = new int[MAX_XOR_VALUE];
23        final int INFINITY = 1 << 30;
24        Arrays.fill(dp, INFINITY);
25        dp[0] = 0; // Base case: 0 changes needed to achieve XOR value 0
26      
27        // Process each group (position modulo k)
28        for (int groupIndex = 0; groupIndex < k; groupIndex++) {
29            // Create new DP array for current iteration
30            int[] nextDp = new int[MAX_XOR_VALUE];
31          
32            // Initialize with worst case: change all elements in current group
33            // plus the minimum cost from previous iteration
34            int minPreviousCost = findMinimum(dp);
35            Arrays.fill(nextDp, minPreviousCost + groupSize[groupIndex]);
36          
37            // Try to optimize by keeping some elements unchanged
38            for (int targetXor = 0; targetXor < MAX_XOR_VALUE; targetXor++) {
39                // For each value that appears in current group
40                for (Map.Entry<Integer, Integer> entry : frequencyMap[groupIndex].entrySet()) {
41                    int value = entry.getKey();
42                    int frequency = entry.getValue();
43                  
44                    // Cost = previous cost + (elements to change in current group)
45                    // We keep 'frequency' elements as 'value', change the rest
46                    int cost = dp[targetXor ^ value] + groupSize[groupIndex] - frequency;
47                    nextDp[targetXor] = Math.min(nextDp[targetXor], cost);
48                }
49            }
50          
51            // Update DP array for next iteration
52            dp = nextDp;
53        }
54      
55        // Return minimum changes needed to make XOR equal to 0
56        return dp[0];
57    }
58  
59    /**
60     * Find the minimum value in an array
61     * @param array the input array
62     * @return the minimum value
63     */
64    private int findMinimum(int[] array) {
65        int minimum = array[0];
66        for (int value : array) {
67            minimum = Math.min(minimum, value);
68        }
69        return minimum;
70    }
71}
72
1class Solution {
2public:
3    int minChanges(vector<int>& nums, int k) {
4        // Maximum possible value (10 bits can represent 0-1023)
5        const int MAX_VALUE = 1 << 10;
6      
7        // Store frequency of each number at each position modulo k
8        // frequencyMap[i] stores frequencies for positions i, i+k, i+2k, ...
9        unordered_map<int, int> frequencyMap[k];
10      
11        // Store the total count of elements at each position modulo k
12        vector<int> groupSize(k);
13      
14        // Build frequency maps for each group (positions with same remainder when divided by k)
15        for (int i = 0; i < nums.size(); ++i) {
16            int groupIndex = i % k;
17            frequencyMap[groupIndex][nums[i]]++;
18            groupSize[groupIndex]++;
19        }
20      
21        // dp[xorValue] = minimum changes needed to achieve XOR value of xorValue
22        // Initialize with a large value (infinity)
23        vector<int> dp(MAX_VALUE, 1 << 30);
24        dp[0] = 0;  // Base case: 0 changes needed to achieve XOR of 0 initially
25      
26        // Process each group (0 to k-1)
27        for (int groupIndex = 0; groupIndex < k; ++groupIndex) {
28            // Find the minimum cost from previous state
29            int minPreviousCost = *min_element(dp.begin(), dp.end());
30          
31            // Create new dp array for current iteration
32            // Initialize with worst case: change all elements in current group
33            vector<int> nextDp(MAX_VALUE, minPreviousCost + groupSize[groupIndex]);
34          
35            // Try to optimize each target XOR value
36            for (int targetXor = 0; targetXor < MAX_VALUE; ++targetXor) {
37                // For each value that appears in current group
38                for (auto& [value, frequency] : frequencyMap[groupIndex]) {
39                    // If we use 'value' for current group to achieve targetXor,
40                    // we need previous XOR to be (targetXor ^ value)
41                    // Cost = previous cost + elements to change in current group
42                    int previousXor = targetXor ^ value;
43                    int costToChange = groupSize[groupIndex] - frequency;
44                    nextDp[targetXor] = min(nextDp[targetXor], 
45                                           dp[previousXor] + costToChange);
46                }
47            }
48          
49            // Move to next iteration
50            dp = move(nextDp);
51        }
52      
53        // Return minimum changes needed to make XOR equal to 0
54        return dp[0];
55    }
56};
57
1function minChanges(nums: number[], k: number): number {
2    // Maximum possible value (10 bits can represent 0-1023)
3    const MAX_VALUE = 1 << 10;
4  
5    // Store frequency of each number at each position modulo k
6    // frequencyMap[i] stores frequencies for positions i, i+k, i+2k, ...
7    const frequencyMap: Map<number, number>[] = Array(k).fill(null).map(() => new Map());
8  
9    // Store the total count of elements at each position modulo k
10    const groupSize: number[] = Array(k).fill(0);
11  
12    // Build frequency maps for each group (positions with same remainder when divided by k)
13    for (let i = 0; i < nums.length; i++) {
14        const groupIndex = i % k;
15        const currentFreq = frequencyMap[groupIndex].get(nums[i]) || 0;
16        frequencyMap[groupIndex].set(nums[i], currentFreq + 1);
17        groupSize[groupIndex]++;
18    }
19  
20    // dp[xorValue] = minimum changes needed to achieve XOR value of xorValue
21    // Initialize with a large value (infinity)
22    let dp: number[] = Array(MAX_VALUE).fill(Number.MAX_SAFE_INTEGER);
23    dp[0] = 0;  // Base case: 0 changes needed to achieve XOR of 0 initially
24  
25    // Process each group (0 to k-1)
26    for (let groupIndex = 0; groupIndex < k; groupIndex++) {
27        // Find the minimum cost from previous state
28        const minPreviousCost = Math.min(...dp);
29      
30        // Create new dp array for current iteration
31        // Initialize with worst case: change all elements in current group
32        const nextDp: number[] = Array(MAX_VALUE).fill(minPreviousCost + groupSize[groupIndex]);
33      
34        // Try to optimize each target XOR value
35        for (let targetXor = 0; targetXor < MAX_VALUE; targetXor++) {
36            // For each value that appears in current group
37            for (const [value, frequency] of frequencyMap[groupIndex].entries()) {
38                // If we use 'value' for current group to achieve targetXor,
39                // we need previous XOR to be (targetXor ^ value)
40                // Cost = previous cost + elements to change in current group
41                const previousXor = targetXor ^ value;
42                const costToChange = groupSize[groupIndex] - frequency;
43                nextDp[targetXor] = Math.min(nextDp[targetXor], 
44                                            dp[previousXor] + costToChange);
45            }
46        }
47      
48        // Move to next iteration
49        dp = nextDp;
50    }
51  
52    // Return minimum changes needed to make XOR equal to 0
53    return dp[0];
54}
55

Time and Space Complexity

Time Complexity: O(2^C × k + n), where n is the length of the array nums, k is the given parameter, and C = 10 (the maximum number of bits needed to represent elements, as indicated by n = 1 << 10 = 1024).

Breaking down the time complexity:

  • Initial setup iterates through all n elements to populate cnt and size: O(n)
  • The main dynamic programming loop runs k iterations
  • In each iteration:
    • Computing min(f) takes O(2^C) time
    • Initializing array g with size 2^C takes O(2^C) time
    • The nested loops iterate through all 2^C states (j loop) and for each state, iterate through all distinct values in cnt[i] (at most O(n/k) values on average)
    • For each of the 2^C states, we perform O(1) operations for each distinct value
  • Since we have k iterations and each processes 2^C states: O(2^C × k)
  • Total: O(2^C × k + n)

Space Complexity: O(2^C × k), where C = 10.

Breaking down the space complexity:

  • cnt array stores k Counter objects, each potentially storing up to O(n/k) distinct values: O(n)
  • size array of length k: O(k)
  • Arrays f and g each have size 2^C = 1024: O(2^C)
  • Total: O(2^C + n), which simplifies to O(2^C × k) when considering that n could be proportional to k in worst case scenarios, and 2^C is the dominant term

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

Common Pitfalls

1. Incorrect Handling of the "Change All" Case

Pitfall: One of the most common mistakes is incorrectly implementing the case where we change all elements in a group to a completely new value (one that doesn't exist in the current group). This optimization is crucial for correctness.

Why it happens: When we change all elements in a group to a new value x, we can choose ANY value for x to achieve ANY target XOR. This gives us maximum flexibility but at the cost of changing all elements in that group.

Incorrect implementation:

# WRONG: Only considering existing values
for position in range(k):
    next_dp = [float('inf')] * MAX_VAL
    for target_xor in range(MAX_VAL):
        for value, count in frequency_by_position[position].items():
            prev_xor = target_xor ^ value
            changes_needed = dp[prev_xor] + position_sizes[position] - count
            next_dp[target_xor] = min(next_dp[target_xor], changes_needed)
    dp = next_dp

Correct implementation:

# CORRECT: Initialize with the "change all" case first
for position in range(k):
    # This allows any target XOR by changing all elements
    next_dp = [min(dp) + position_sizes[position]] * MAX_VAL
  
    # Then optimize with existing values
    for target_xor in range(MAX_VAL):
        for value, count in frequency_by_position[position].items():
            prev_xor = target_xor ^ value
            changes_needed = dp[prev_xor] + position_sizes[position] - count
            next_dp[target_xor] = min(next_dp[target_xor], changes_needed)
    dp = next_dp

2. Memory/Time Limit Issues with Large Value Ranges

Pitfall: Using a fixed MAX_VAL = 1 << 10 without considering the actual constraints can lead to unnecessary memory usage or even wrong answers if values exceed this range.

Solution: Determine the actual maximum value needed based on the problem constraints or the input data:

# Better approach: determine actual range needed
max_val_in_array = max(nums) if nums else 0
MAX_VAL = 1
while MAX_VAL <= max_val_in_array:
    MAX_VAL <<= 1
# Or simply: MAX_VAL = 1 << (max_val_in_array.bit_length() + 1)

3. XOR Property Confusion

Pitfall: Misunderstanding the XOR transition formula. When setting group i to value v and wanting final XOR to be target_xor, the required previous XOR is target_xor ^ v, NOT target_xor - v or any other operation.

Why: This works because XOR is self-inverse: (a ^ b) ^ b = a. So if previous XOR is prev and we add value v, the new XOR is prev ^ v. To get target, we need prev ^ v = target, which means prev = target ^ v.

4. Edge Case: Empty Groups

Pitfall: Not handling the case where len(nums) < k or when some positions in the cycle have no elements.

Solution: The current implementation handles this naturally since position_sizes[i] will be 0 for empty positions, but be aware that in some variations of this problem, you might need explicit handling:

# Explicit check if needed
if len(nums) < k:
    # Special handling for when array is shorter than k
    return len(nums)  # All elements must be changed to 0

5. Integer Overflow in Other Languages

Pitfall: If implementing in languages like C++ or Java, the infinity value and arithmetic operations might overflow.

Solution for other languages:

// C++ example
const int INF = 1e9;  // Use a large but safe value instead of INT_MAX
// Avoid: const int INF = INT_MAX; // Can cause overflow when adding
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following problems can be solved with backtracking (select multiple)


Recommended Readings

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

Load More