3599. Partition Array to Minimize XOR
Problem Description
You have an integer array nums
and an integer k
. Your goal is to split the array into exactly k
non-empty subarrays (consecutive elements).
For each subarray, you calculate the XOR of all its elements. Among all k
XOR values obtained, you take the maximum value.
Your task is to find the minimum possible value of this maximum XOR across all valid ways to partition the array.
Example walkthrough:
If nums = [1, 2, 3, 4]
and k = 2
, you could partition it as:
[1, 2]
and[3, 4]
: XOR values are1 ^ 2 = 3
and3 ^ 4 = 7
, maximum is7
[1]
and[2, 3, 4]
: XOR values are1
and2 ^ 3 ^ 4 = 5
, maximum is5
[1, 2, 3]
and[4]
: XOR values are1 ^ 2 ^ 3 = 0
and4
, maximum is4
Among all partitions, the minimum of these maximum values would be the answer.
Key points:
- Each subarray must be non-empty and consecutive
- You must use exactly
k
subarrays - The entire array must be covered by the
k
subarrays - XOR operation: For a subarray
[a, b, c]
, the XOR isa ^ b ^ c
- You want to minimize the largest XOR value among all
k
subarrays
Intuition
When we need to partition an array into subarrays with some optimization goal, dynamic programming often comes to mind. The key insight is that we can build up the solution by considering smaller subproblems.
Let's think about what information we need to track. For any position in the array, we need to know:
- How many elements we've processed so far
- How many subarrays we've created so far
- What's the minimum possible maximum XOR value achievable
This naturally leads to defining a state f[i][j]
representing "the minimum possible maximum XOR when partitioning the first i
elements into exactly j
subarrays."
To compute f[i][j]
, we need to consider all possible positions where the last subarray could start. If the last subarray starts at position h+1
and ends at position i
, then:
- The previous
h
elements must be partitioned intoj-1
subarrays - The XOR of the last subarray is the XOR of elements from
h+1
toi
- The overall maximum XOR is the max of the previous maximum (from
f[h][j-1]
) and the current subarray's XOR
To efficiently calculate XOR values for any subarray, we use prefix XOR. If g[i]
stores the XOR of the first i
elements, then the XOR of elements from position h+1
to i
is simply g[i] ^ g[h]
. This works because XOR has the property that a ^ a = 0
, so the elements before position h+1
cancel out.
The base case is f[0][0] = 0
(zero elements partitioned into zero subarrays has no XOR values, so we can consider the maximum as 0). All other states start at infinity since they haven't been computed yet.
By iterating through all positions i
, all possible number of partitions j
, and all possible starting positions h
for the last subarray, we can fill the DP table and eventually find f[n][k]
- the answer to our problem.
Learn more about Dynamic Programming and Prefix Sum patterns.
Solution Approach
We implement the dynamic programming solution using a 2D table and prefix XOR array.
Step 1: Initialize Prefix XOR Array
First, we create a prefix XOR array g
where g[i]
stores the XOR of the first i
elements:
g = [0] * (n + 1)
for i, x in enumerate(nums, 1):
g[i] = g[i - 1] ^ x
This allows us to compute the XOR of any subarray [h+1...i]
as g[i] ^ g[h]
in O(1) time.
Step 2: Initialize DP Table
Create a 2D DP table f
where f[i][j]
represents the minimum possible maximum XOR when partitioning the first i
elements into j
subarrays:
f = [[inf] * (k + 1) for _ in range(n + 1)]
f[0][0] = 0
We initialize all values to infinity except f[0][0] = 0
(base case: zero elements into zero subarrays).
Step 3: Fill the DP Table
We iterate through three nested loops:
i
from 1 ton
: number of elements consideredj
from 1 tomin(i, k)
: number of partitions (can't have more partitions than elements)h
fromj-1
toi-1
: ending position of the previous subarray
For each combination, we update f[i][j]
using the state transition equation:
f[i][j] = min(f[i][j], max(f[h][j - 1], g[i] ^ g[h]))
The equation works as follows:
f[h][j-1]
: the minimum maximum XOR for the firsth
elements partitioned intoj-1
subarraysg[i] ^ g[h]
: the XOR of the current subarray from positionh+1
toi
max(...)
: the maximum XOR among allj
subarrays (including the new one)min(...)
: we keep the minimum among all possible ways to place the last subarray
Step 4: Return the Result
After filling the entire table, f[n][k]
contains the minimum possible maximum XOR when partitioning all n
elements into exactly k
subarrays.
Time Complexity: O(n² × k) where n is the length of the array Space Complexity: O(n × k) for the DP table
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 = [3, 9, 5]
and k = 2
.
Step 1: Initialize Prefix XOR Array
g[0] = 0
g[1] = 0 ^ 3 = 3
g[2] = 3 ^ 9 = 10
g[3] = 10 ^ 5 = 15
This gives us g = [0, 3, 10, 15]
. Now we can compute any subarray XOR quickly:
- XOR of
[3]
:g[1] ^ g[0] = 3 ^ 0 = 3
- XOR of
[9, 5]
:g[3] ^ g[1] = 15 ^ 3 = 12
- XOR of
[3, 9]
:g[2] ^ g[0] = 10 ^ 0 = 10
Step 2: Initialize DP Table
f[i][j] where i = 0 to 3, j = 0 to 2 Initial state (all inf except f[0][0] = 0): j=0 j=1 j=2 i=0 0 inf inf i=1 inf inf inf i=2 inf inf inf i=3 inf inf inf
Step 3: Fill the DP Table
For i=1, j=1
: First element into 1 subarray
h
can only be 0 (previous 0 elements in 0 subarrays)f[1][1] = min(inf, max(f[0][0], g[1] ^ g[0])) = max(0, 3) = 3
For i=2, j=1
: First 2 elements into 1 subarray
h
can only be 0f[2][1] = min(inf, max(f[0][0], g[2] ^ g[0])) = max(0, 10) = 10
For i=2, j=2
: First 2 elements into 2 subarrays
h
can only be 1 (split after first element)f[2][2] = min(inf, max(f[1][1], g[2] ^ g[1])) = max(3, 10 ^ 3) = max(3, 9) = 9
For i=3, j=1
: All 3 elements into 1 subarray
h
can only be 0f[3][1] = min(inf, max(f[0][0], g[3] ^ g[0])) = max(0, 15) = 15
For i=3, j=2
: All 3 elements into 2 subarrays
h=1
: Split as[3]
and[9, 5]
f[3][2] = min(inf, max(f[1][1], g[3] ^ g[1])) = max(3, 12) = 12
h=2
: Split as[3, 9]
and[5]
f[3][2] = min(12, max(f[2][1], g[3] ^ g[2])) = min(12, max(10, 5)) = min(12, 10) = 10
Final DP Table:
j=0 j=1 j=2 i=0 0 inf inf i=1 inf 3 inf i=2 inf 10 9 i=3 inf 15 10
Step 4: Return Result
f[3][2] = 10
, which means the minimum possible maximum XOR when splitting [3, 9, 5]
into 2 subarrays is 10.
This corresponds to the partition [3, 9]
and [5]
:
- XOR of
[3, 9]
=3 ^ 9 = 10
- XOR of
[5]
=5
- Maximum = 10
Solution Implementation
1from typing import List
2from math import inf
3
4
5class Solution:
6 def minXor(self, nums: List[int], k: int) -> int:
7 """
8 Find the minimum possible maximum XOR value when partitioning
9 the array into exactly k subarrays.
10
11 Args:
12 nums: List of integers to partition
13 k: Number of subarrays to create
14
15 Returns:
16 Minimum possible maximum XOR value among all subarrays
17 """
18 n = len(nums)
19
20 # Build prefix XOR array
21 # prefix_xor[i] = XOR of all elements from index 0 to i-1
22 prefix_xor = [0] * (n + 1)
23 for i, num in enumerate(nums, 1):
24 prefix_xor[i] = prefix_xor[i - 1] ^ num
25
26 # Initialize DP table
27 # dp[i][j] = minimum maximum XOR value when partitioning nums[0:i] into j subarrays
28 dp = [[inf] * (k + 1) for _ in range(n + 1)]
29 dp[0][0] = 0 # Base case: 0 elements in 0 subarrays has XOR value 0
30
31 # Fill DP table
32 for i in range(1, n + 1):
33 # Can't partition i elements into more than i subarrays
34 for j in range(1, min(i, k) + 1):
35 # Try all possible positions for the last subarray
36 # Last subarray spans from index h to i-1
37 for h in range(j - 1, i):
38 # XOR of subarray from h to i-1 is prefix_xor[i] ^ prefix_xor[h]
39 current_xor = prefix_xor[i] ^ prefix_xor[h]
40 # Take maximum of previous result and current subarray XOR
41 max_xor = max(dp[h][j - 1], current_xor)
42 # Update minimum possible maximum XOR
43 dp[i][j] = min(dp[i][j], max_xor)
44
45 return dp[n][k]
46
1class Solution {
2 public int minXor(int[] nums, int k) {
3 int arrayLength = nums.length;
4
5 // prefixXor[i] stores XOR of all elements from index 0 to i-1
6 // This allows us to compute XOR of any subarray in O(1) time
7 int[] prefixXor = new int[arrayLength + 1];
8 for (int i = 1; i <= arrayLength; ++i) {
9 prefixXor[i] = prefixXor[i - 1] ^ nums[i - 1];
10 }
11
12 // dp[i][j] represents the minimum maximum XOR value when dividing
13 // the first i elements into j subarrays
14 int[][] dp = new int[arrayLength + 1][k + 1];
15
16 // Initialize all values to maximum integer (infinity)
17 for (int i = 0; i <= arrayLength; ++i) {
18 Arrays.fill(dp[i], Integer.MAX_VALUE);
19 }
20
21 // Base case: 0 elements divided into 0 subarrays has XOR value 0
22 dp[0][0] = 0;
23
24 // Fill the DP table
25 for (int i = 1; i <= arrayLength; ++i) {
26 // Can't divide i elements into more than i subarrays
27 for (int j = 1; j <= Math.min(i, k); ++j) {
28 // Try all possible positions for the last subarray
29 // Last subarray could start from index h to index i-1
30 for (int lastSubarrayStart = j - 1; lastSubarrayStart < i; ++lastSubarrayStart) {
31 // XOR of last subarray: from lastSubarrayStart to i-1
32 int lastSubarrayXor = prefixXor[i] ^ prefixXor[lastSubarrayStart];
33
34 // Update dp[i][j] with the minimum of current value and
35 // the maximum of (previous subarrays' result, current subarray XOR)
36 dp[i][j] = Math.min(dp[i][j],
37 Math.max(dp[lastSubarrayStart][j - 1], lastSubarrayXor));
38 }
39 }
40 }
41
42 // Return the minimum maximum XOR when dividing all n elements into k subarrays
43 return dp[arrayLength][k];
44 }
45}
46
1class Solution {
2public:
3 int minXor(vector<int>& nums, int k) {
4 int n = nums.size();
5
6 // prefixXor[i] stores XOR of all elements from index 0 to i-1
7 // This allows us to compute XOR of any subarray in O(1) time
8 vector<int> prefixXor(n + 1);
9 for (int i = 1; i <= n; ++i) {
10 prefixXor[i] = prefixXor[i - 1] ^ nums[i - 1];
11 }
12
13 // dp[i][j] represents the minimum possible maximum XOR value
14 // when partitioning first i elements into j subarrays
15 const int INF = numeric_limits<int>::max();
16 vector<vector<int>> dp(n + 1, vector<int>(k + 1, INF));
17
18 // Base case: 0 elements partitioned into 0 subarrays has cost 0
19 dp[0][0] = 0;
20
21 // Fill the DP table
22 for (int i = 1; i <= n; ++i) {
23 // Can't partition i elements into more than i subarrays
24 for (int j = 1; j <= min(i, k); ++j) {
25 // Try all possible positions for the last subarray
26 // Last subarray spans from index h to i-1
27 for (int h = j - 1; h < i; ++h) {
28 // XOR of subarray from h to i-1 is prefixXor[i] ^ prefixXor[h]
29 int currentSubarrayXor = prefixXor[i] ^ prefixXor[h];
30
31 // Update dp[i][j] with the minimum of:
32 // - current value
33 // - maximum of (best solution for first h elements with j-1 subarrays,
34 // XOR of current subarray)
35 dp[i][j] = min(dp[i][j], max(dp[h][j - 1], currentSubarrayXor));
36 }
37 }
38 }
39
40 // Return the minimum maximum XOR when partitioning all n elements into k subarrays
41 return dp[n][k];
42 }
43};
44
1/**
2 * Finds the minimum possible largest XOR value when dividing an array into k subarrays
3 * @param nums - The input array of numbers
4 * @param k - The number of subarrays to divide into
5 * @returns The minimum possible largest XOR value among all k subarrays
6 */
7function minXor(nums: number[], k: number): number {
8 const arrayLength: number = nums.length;
9
10 // prefixXor[i] stores the XOR of all elements from index 0 to i-1
11 // This allows us to calculate XOR of any subarray in O(1) time
12 const prefixXor: number[] = Array(arrayLength + 1).fill(0);
13 for (let i = 1; i <= arrayLength; ++i) {
14 prefixXor[i] = prefixXor[i - 1] ^ nums[i - 1];
15 }
16
17 const INFINITY: number = Number.MAX_SAFE_INTEGER;
18
19 // dp[i][j] represents the minimum possible largest XOR value
20 // when dividing the first i elements into j subarrays
21 const dp: number[][] = Array.from(
22 { length: arrayLength + 1 },
23 () => Array(k + 1).fill(INFINITY)
24 );
25
26 // Base case: 0 elements divided into 0 subarrays has XOR value of 0
27 dp[0][0] = 0;
28
29 // Fill the DP table
30 for (let currentIndex = 1; currentIndex <= arrayLength; ++currentIndex) {
31 // Can't divide into more subarrays than elements available
32 for (let subarrayCount = 1; subarrayCount <= Math.min(currentIndex, k); ++subarrayCount) {
33 // Try all possible positions for the last subarray's starting point
34 for (let lastSubarrayStart = subarrayCount - 1; lastSubarrayStart < currentIndex; ++lastSubarrayStart) {
35 // XOR value of the last subarray (from lastSubarrayStart to currentIndex-1)
36 const currentSubarrayXor: number = prefixXor[currentIndex] ^ prefixXor[lastSubarrayStart];
37
38 // Update minimum by taking max of previous subarrays' result and current subarray's XOR
39 dp[currentIndex][subarrayCount] = Math.min(
40 dp[currentIndex][subarrayCount],
41 Math.max(dp[lastSubarrayStart][subarrayCount - 1], currentSubarrayXor)
42 );
43 }
44 }
45 }
46
47 return dp[arrayLength][k];
48}
49
Time and Space Complexity
Time Complexity: O(n^2 × k)
The time complexity is determined by the triple nested loop structure:
- The outer loop iterates through
i
from1
ton
(n iterations) - The middle loop iterates through
j
from1
tomin(i, k)
(at most k iterations) - The inner loop iterates through
h
fromj-1
toi-1
(at most n iterations)
For each combination of (i, j, h)
, the operations performed (XOR, min, max comparisons) are O(1)
. Therefore, the overall time complexity is O(n × k × n) = O(n^2 × k)
.
Space Complexity: O(n × k)
The space complexity comes from:
- Array
g
of sizen+1
:O(n)
- 2D DP array
f
of size(n+1) × (k+1)
:O(n × k)
The dominant space requirement is the 2D array f
, giving us a total space complexity of O(n × k)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Base Case Initialization
Pitfall: A common mistake is incorrectly initializing the DP table, particularly forgetting to set dp[0][0] = 0
or mistakenly setting other base cases like dp[i][0] = 0
for all i
.
Why it's wrong:
dp[i][0]
fori > 0
should remain infinity because you cannot partition a non-empty array into 0 subarrays- Only
dp[0][0] = 0
is valid (0 elements into 0 subarrays)
Solution:
# Correct initialization
dp = [[inf] * (k + 1) for _ in range(n + 1)]
dp[0][0] = 0 # Only this base case is valid
# Incorrect (don't do this)
# for i in range(n + 1):
# dp[i][0] = 0 # Wrong! Can't partition non-empty array into 0 subarrays
2. Off-by-One Errors in Loop Boundaries
Pitfall: Using incorrect loop ranges, especially for the variable h
which represents the split point.
Why it's wrong:
- If
h
starts fromj
instead ofj-1
, you miss valid partitions - If
h
goes up toi
instead ofi-1
, you create empty subarrays
Solution:
# Correct: h ranges from j-1 to i-1
for h in range(j - 1, i):
current_xor = prefix_xor[i] ^ prefix_xor[h]
# This creates subarray from index h to i-1
# Incorrect examples:
# for h in range(j, i): # Misses valid partitions
# for h in range(j - 1, i + 1): # Creates empty subarray when h = i
3. Prefix XOR Calculation Error
Pitfall: Computing the XOR of a subarray incorrectly by using wrong indices with the prefix XOR array.
Why it's wrong:
- The XOR of subarray from index
left
toright
(inclusive) should beprefix_xor[right + 1] ^ prefix_xor[left]
- In our DP, the subarray from position
h
toi-1
should useprefix_xor[i] ^ prefix_xor[h]
Solution:
# Correct: XOR of elements from index h to i-1 current_xor = prefix_xor[i] ^ prefix_xor[h] # Incorrect (common mistakes): # current_xor = prefix_xor[i] ^ prefix_xor[h - 1] # Wrong indices # current_xor = prefix_xor[i - 1] ^ prefix_xor[h] # Wrong indices
4. Not Handling Edge Cases
Pitfall: Failing to handle edge cases like when k = 1
(entire array as one subarray) or k = n
(each element as its own subarray).
Solution:
def minXor(self, nums: List[int], k: int) -> int:
n = len(nums)
# Edge case: if k equals n, each element is its own subarray
if k == n:
return max(nums)
# Edge case: if k equals 1, return XOR of entire array
if k == 1:
result = 0
for num in nums:
result ^= num
return result
# Continue with normal DP solution...
5. Memory Optimization Opportunity Missed
Pitfall: Not recognizing that we only need the previous row of the DP table, leading to unnecessary space usage.
Solution (Space-Optimized):
# Instead of 2D array, use two 1D arrays
prev = [inf] * (k + 1)
curr = [inf] * (k + 1)
prev[0] = 0
for i in range(1, n + 1):
curr = [inf] * (k + 1)
for j in range(1, min(i, k) + 1):
for h in range(j - 1, i):
current_xor = prefix_xor[i] ^ prefix_xor[h]
if h == 0:
max_xor = current_xor
else:
max_xor = max(prev[j - 1] if h == i - 1 else dp[h][j - 1], current_xor)
curr[j] = min(curr[j], max_xor)
prev = curr
What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.
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
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
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
Want a Structured Path to Master System Design Too? Don’t Miss This!