Facebook Pixel

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 are 1 ^ 2 = 3 and 3 ^ 4 = 7, maximum is 7
  • [1] and [2, 3, 4]: XOR values are 1 and 2 ^ 3 ^ 4 = 5, maximum is 5
  • [1, 2, 3] and [4]: XOR values are 1 ^ 2 ^ 3 = 0 and 4, maximum is 4

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 is a ^ b ^ c
  • You want to minimize the largest XOR value among all k subarrays
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. How many elements we've processed so far
  2. How many subarrays we've created so far
  3. 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 into j-1 subarrays
  • The XOR of the last subarray is the XOR of elements from h+1 to i
  • 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 to n: number of elements considered
  • j from 1 to min(i, k): number of partitions (can't have more partitions than elements)
  • h from j-1 to i-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 first h elements partitioned into j-1 subarrays
  • g[i] ^ g[h]: the XOR of the current subarray from position h+1 to i
  • max(...): the maximum XOR among all j 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 Evaluator

Example 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 0
  • f[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 0
  • f[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 from 1 to n (n iterations)
  • The middle loop iterates through j from 1 to min(i, k) (at most k iterations)
  • The inner loop iterates through h from j-1 to i-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 size n+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] for i > 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 from j instead of j-1, you miss valid partitions
  • If h goes up to i instead of i-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 to right (inclusive) should be prefix_xor[right + 1] ^ prefix_xor[left]
  • In our DP, the subarray from position h to i-1 should use prefix_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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

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

Load More