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.
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:
- Changing all elements in the group to a completely new value (useful when no existing value in the group is beneficial)
- 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 groupi
size[i]
stores the total number of elements in groupi
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:
-
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 groupi
- We take the minimum cost from any previous XOR value (
min(f)
) and addsize[i]
(changing all elements)
-
Option 2 - Use an existing value from the group:
- For each possible target XOR value
j
and each existing valuev
in groupi
- If we set group
i
to valuev
, and we want final XOR to bej
, then previous groups must have XOR ofj ^ 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 valuev
in groupi
- For each possible target XOR value
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 EvaluatorExample 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:
- Change all elements in group 0 to a new value: cost =
min(f) + size[0] = 0 + 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 populatecnt
andsize
:O(n)
- The main dynamic programming loop runs
k
iterations - In each iteration:
- Computing
min(f)
takesO(2^C)
time - Initializing array
g
with size2^C
takesO(2^C)
time - The nested loops iterate through all
2^C
states (j
loop) and for each state, iterate through all distinct values incnt[i]
(at mostO(n/k)
values on average) - For each of the
2^C
states, we performO(1)
operations for each distinct value
- Computing
- Since we have
k
iterations and each processes2^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 storesk
Counter objects, each potentially storing up toO(n/k)
distinct values:O(n)
size
array of lengthk
:O(k)
- Arrays
f
andg
each have size2^C = 1024
:O(2^C)
- Total:
O(2^C + n)
, which simplifies toO(2^C × k)
when considering thatn
could be proportional tok
in worst case scenarios, and2^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
Which of the following problems can be solved with backtracking (select multiple)
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!