Facebook Pixel

1310. XOR Queries of a Subarray

MediumBit ManipulationArrayPrefix Sum
Leetcode Link

Problem Description

You are given an array arr containing positive integers and an array queries where each query is represented as queries[i] = [left_i, right_i].

For each query i, you need to calculate the XOR of all elements in the subarray from index left_i to index right_i (inclusive). In other words, compute arr[left_i] XOR arr[left_i + 1] XOR ... XOR arr[right_i].

Your task is to return an array answer where answer[i] contains the XOR result for the i-th query.

The solution uses a prefix XOR technique. A prefix XOR array s of length n+1 is created where s[i] stores the XOR of all elements from index 0 to i-1 in the original array. This means s[i] = arr[0] XOR arr[1] XOR ... XOR arr[i-1].

The key insight is that for any query [l, r], the XOR of elements from index l to r can be calculated as:

arr[l] XOR arr[l+1] XOR ... XOR arr[r] = s[r+1] XOR s[l]

This works because XORing the same value twice cancels it out. When we compute s[r+1] XOR s[l], all elements before index l appear in both prefix values and cancel each other out, leaving only the XOR of elements from l to r.

The implementation uses Python's accumulate function with the xor operator to build the prefix XOR array efficiently, then applies the formula s[r+1] XOR s[l] for each query to generate the results.

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

Intuition

The naive approach would be to iterate through each query and XOR all elements in the specified range. However, this would be inefficient, especially when we have many queries on the same array.

The key observation is that XOR has a special property: a XOR a = 0. This means XORing a value with itself cancels out to zero. Additionally, XOR is both associative and commutative, meaning we can rearrange and group XOR operations however we want.

Let's think about how we can use these properties. If we want to find the XOR of elements from index l to r, we can express it as:

arr[l] XOR arr[l+1] XOR ... XOR arr[r]

Now, imagine we have already computed the XOR of all elements from index 0 to r and stored it somewhere. Let's call this prefix_xor[r+1]. Similarly, we have the XOR of all elements from index 0 to l-1, which we'll call prefix_xor[l].

Here's the clever part: If we XOR these two prefix values together:

prefix_xor[r+1] XOR prefix_xor[l] = (arr[0] XOR ... XOR arr[r]) XOR (arr[0] XOR ... XOR arr[l-1])

The elements from index 0 to l-1 appear in both expressions. Since a XOR a = 0, these elements cancel out, leaving us with exactly the elements from index l to r.

This transforms our problem from repeatedly calculating XOR over ranges to simply preprocessing the array once to build a prefix XOR array, then answering each query in constant time using the formula s[r+1] XOR s[l].

This technique is similar to prefix sums but leverages the self-inverse property of XOR to eliminate unwanted elements from our calculation.

Learn more about Prefix Sum patterns.

Solution Approach

The solution implements the prefix XOR technique to efficiently answer multiple range XOR queries.

Step 1: Build the Prefix XOR Array

We create a prefix XOR array s where s[i] represents the XOR of all elements from index 0 to i-1 in the original array. The array s has length n+1 where n is the length of the input array.

The relationship is: s[i] = s[i-1] XOR arr[i-1]

In the code, this is accomplished using Python's accumulate function:

s = list(accumulate(arr, xor, initial=0))

The accumulate function with the xor operator builds the prefix array iteratively:

  • s[0] = 0 (initial value)
  • s[1] = s[0] XOR arr[0] = 0 XOR arr[0] = arr[0]
  • s[2] = s[1] XOR arr[1] = arr[0] XOR arr[1]
  • s[3] = s[2] XOR arr[2] = arr[0] XOR arr[1] XOR arr[2]
  • And so on...

Step 2: Answer Each Query

For each query [l, r], we need to find the XOR of elements from index l to r. Using the mathematical property derived earlier:

arr[l] XOR arr[l+1] XOR ... XOR arr[r] = s[r+1] XOR s[l]

This works because:

  • s[r+1] contains the XOR of all elements from index 0 to r
  • s[l] contains the XOR of all elements from index 0 to l-1
  • When we XOR these two values, the elements from 0 to l-1 cancel out (since a XOR a = 0), leaving only the XOR of elements from l to r

The code uses a list comprehension to process all queries:

return [s[r + 1] ^ s[l] for l, r in queries]

Time and Space Complexity:

  • Time: O(n + q) where n is the length of the array and q is the number of queries
    • O(n) to build the prefix XOR array
    • O(1) for each query, so O(q) for all queries
  • Space: O(n) for storing the prefix XOR array

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to understand how the prefix XOR solution works.

Given:

  • arr = [1, 3, 4, 8]
  • queries = [[0, 1], [1, 2], [0, 3], [3, 3]]

Step 1: Build the Prefix XOR Array

We create the prefix XOR array s where each element stores the XOR of all elements up to that point:

  • s[0] = 0 (initial value)
  • s[1] = s[0] XOR arr[0] = 0 XOR 1 = 1
  • s[2] = s[1] XOR arr[1] = 1 XOR 3 = 2
  • s[3] = s[2] XOR arr[2] = 2 XOR 4 = 6
  • s[4] = s[3] XOR arr[3] = 6 XOR 8 = 14

So our prefix XOR array is: s = [0, 1, 2, 6, 14]

Step 2: Process Each Query

Now we can answer each query using the formula: result = s[r+1] XOR s[l]

Query 1: [0, 1] - Find XOR of elements from index 0 to 1

  • We need: arr[0] XOR arr[1] = 1 XOR 3 = 2
  • Using our formula: s[1+1] XOR s[0] = s[2] XOR s[0] = 2 XOR 0 = 2

Query 2: [1, 2] - Find XOR of elements from index 1 to 2

  • We need: arr[1] XOR arr[2] = 3 XOR 4 = 7
  • Using our formula: s[2+1] XOR s[1] = s[3] XOR s[1] = 6 XOR 1 = 7

Query 3: [0, 3] - Find XOR of elements from index 0 to 3

  • We need: arr[0] XOR arr[1] XOR arr[2] XOR arr[3] = 1 XOR 3 XOR 4 XOR 8 = 14
  • Using our formula: s[3+1] XOR s[0] = s[4] XOR s[0] = 14 XOR 0 = 14

Query 4: [3, 3] - Find XOR of a single element at index 3

  • We need: arr[3] = 8
  • Using our formula: s[3+1] XOR s[3] = s[4] XOR s[3] = 14 XOR 6 = 8

Final Answer: [2, 7, 14, 8]

The beauty of this approach is that after building the prefix array once in O(n) time, we can answer each query in O(1) time, regardless of the range size.

Solution Implementation

1from typing import List
2from itertools import accumulate
3from operator import xor
4
5
6class Solution:
7    def xorQueries(self, arr: List[int], queries: List[List[int]]) -> List[int]:
8        # Build prefix XOR array where prefix_xor[i] = arr[0] ^ arr[1] ^ ... ^ arr[i-1]
9        # Initial value of 0 ensures prefix_xor[0] = 0 (XOR of empty array)
10        prefix_xor = list(accumulate(arr, xor, initial=0))
11      
12        # For each query [left, right], calculate XOR of subarray arr[left:right+1]
13        # Using property: XOR(arr[left:right+1]) = prefix_xor[right+1] ^ prefix_xor[left]
14        # This works because XOR is self-inverse: a ^ a = 0
15        result = []
16        for left, right in queries:
17            subarray_xor = prefix_xor[right + 1] ^ prefix_xor[left]
18            result.append(subarray_xor)
19      
20        return result
21
1class Solution {
2    public int[] xorQueries(int[] arr, int[][] queries) {
3        // Get the length of the input array
4        int arrayLength = arr.length;
5      
6        // Create prefix XOR array with size n+1 for easier calculation
7        // prefixXor[i] stores XOR of all elements from index 0 to i-1
8        int[] prefixXor = new int[arrayLength + 1];
9      
10        // Build the prefix XOR array
11        // prefixXor[i] = arr[0] ^ arr[1] ^ ... ^ arr[i-1]
12        for (int i = 1; i <= arrayLength; i++) {
13            prefixXor[i] = prefixXor[i - 1] ^ arr[i - 1];
14        }
15      
16        // Get the number of queries to process
17        int queryCount = queries.length;
18      
19        // Initialize result array to store XOR results for each query
20        int[] result = new int[queryCount];
21      
22        // Process each query
23        for (int i = 0; i < queryCount; i++) {
24            // Extract left and right boundaries of the current query
25            int left = queries[i][0];
26            int right = queries[i][1];
27          
28            // Calculate XOR for range [left, right] using prefix XOR
29            // XOR(left to right) = prefixXor[right+1] ^ prefixXor[left]
30            // This works because XOR is self-inverse: a ^ a = 0
31            result[i] = prefixXor[right + 1] ^ prefixXor[left];
32        }
33      
34        // Return the array containing XOR results for all queries
35        return result;
36    }
37}
38
1class Solution {
2public:
3    vector<int> xorQueries(vector<int>& arr, vector<vector<int>>& queries) {
4        int arraySize = arr.size();
5      
6        // Create prefix XOR array with size n+1
7        // prefixXor[i] stores XOR of all elements from arr[0] to arr[i-1]
8        vector<int> prefixXor(arraySize + 1, 0);
9      
10        // Build prefix XOR array
11        // prefixXor[i] = arr[0] ^ arr[1] ^ ... ^ arr[i-1]
12        for (int i = 1; i <= arraySize; ++i) {
13            prefixXor[i] = prefixXor[i - 1] ^ arr[i - 1];
14        }
15      
16        // Process each query and store results
17        vector<int> result;
18        for (const auto& query : queries) {
19            int left = query[0];
20            int right = query[1];
21          
22            // XOR of elements from arr[left] to arr[right] can be calculated as:
23            // prefixXor[right + 1] ^ prefixXor[left]
24            // This works because XOR is self-inverse: a ^ a = 0
25            result.push_back(prefixXor[right + 1] ^ prefixXor[left]);
26        }
27      
28        return result;
29    }
30};
31
1/**
2 * Processes XOR queries on an array
3 * @param arr - The input array of numbers
4 * @param queries - Array of query ranges, each query is [left, right]
5 * @returns Array of XOR results for each query
6 */
7function xorQueries(arr: number[], queries: number[][]): number[] {
8    const arrayLength: number = arr.length;
9  
10    // Build prefix XOR array where prefixXor[i] = arr[0] ^ arr[1] ^ ... ^ arr[i-1]
11    // prefixXor[0] = 0 (XOR of empty array)
12    const prefixXor: number[] = Array(arrayLength + 1).fill(0);
13  
14    // Calculate prefix XOR values
15    for (let i = 0; i < arrayLength; ++i) {
16        prefixXor[i + 1] = prefixXor[i] ^ arr[i];
17    }
18  
19    // Process each query using the prefix XOR array
20    // XOR from arr[left] to arr[right] = prefixXor[right + 1] ^ prefixXor[left]
21    return queries.map(([left, right]) => prefixXor[right + 1] ^ prefixXor[left]);
22}
23

Time and Space Complexity

The time complexity is O(n + m), where n is the length of the array arr and m is the length of the query array queries.

  • Building the prefix XOR array using accumulate takes O(n) time as it processes each element of arr once.
  • Processing all queries takes O(m) time since each query is answered in O(1) time using the precomputed prefix XOR values.

The space complexity is O(n).

  • The prefix XOR array s stores n + 1 elements (including the initial value 0), which requires O(n) space.
  • The output list stores m results, but this is typically not counted as auxiliary space since it's the required output.

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

Common Pitfalls

1. Off-by-One Errors with Prefix Array Indexing

The most common mistake is misunderstanding the indexing relationship between the prefix XOR array and the original array. Since prefix_xor[i] stores the XOR of elements from index 0 to i-1, many developers incorrectly use prefix_xor[r] ^ prefix_xor[l] or prefix_xor[r] ^ prefix_xor[l-1] for a query [l, r].

Incorrect Implementation:

# Wrong: This would miss arr[r] in the result
result = prefix_xor[r] ^ prefix_xor[l]

# Wrong: This would cause index out of bounds when l=0
result = prefix_xor[r+1] ^ prefix_xor[l-1]

Correct Implementation:

# Correct: prefix_xor[r+1] contains XOR up to arr[r]
# prefix_xor[l] contains XOR up to arr[l-1]
result = prefix_xor[r+1] ^ prefix_xor[l]

2. Forgetting the Initial Value in Prefix Array

When building the prefix XOR array manually without using accumulate, developers often forget to include the initial 0 value, leading to incorrect array size and wrong query results.

Incorrect Implementation:

# Wrong: Missing initial 0 value
prefix_xor = [arr[0]]
for i in range(1, len(arr)):
    prefix_xor.append(prefix_xor[-1] ^ arr[i])
# This creates an array of size n instead of n+1

Correct Implementation:

# Correct: Start with 0 as the first element
prefix_xor = [0]
for num in arr:
    prefix_xor.append(prefix_xor[-1] ^ num)
# This creates an array of size n+1

3. Using Wrong Operator or Confusing XOR with Other Operations

Some developers might accidentally use the wrong bitwise operator or confuse XOR with other operations like OR or AND.

Incorrect Implementation:

# Wrong: Using OR instead of XOR
prefix_xor = list(accumulate(arr, lambda x, y: x | y, initial=0))

# Wrong: Using addition instead of XOR
prefix_xor = list(accumulate(arr, lambda x, y: x + y, initial=0))

Correct Implementation:

# Correct: Using XOR operator
from operator import xor
prefix_xor = list(accumulate(arr, xor, initial=0))
# Or using lambda
prefix_xor = list(accumulate(arr, lambda x, y: x ^ y, initial=0))

4. Not Handling Edge Cases

Failing to consider edge cases like single-element queries or queries at array boundaries.

Potential Issues:

  • Query [0, 0] should return arr[0]
  • Query [n-1, n-1] should return arr[n-1] where n is the array length
  • Empty array input (though typically not given in the problem constraints)

Robust Solution:

def xorQueries(self, arr: List[int], queries: List[List[int]]) -> List[int]:
    if not arr:  # Handle empty array if needed
        return []
  
    prefix_xor = list(accumulate(arr, xor, initial=0))
    result = []
  
    for left, right in queries:
        # Validate query bounds if needed
        if 0 <= left <= right < len(arr):
            result.append(prefix_xor[right + 1] ^ prefix_xor[left])
        else:
            # Handle invalid query appropriately
            result.append(0)  # or raise an exception
  
    return result
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using the following tree as input?

1def serialize(root):
2    res = []
3    def dfs(root):
4        if not root:
5            res.append('x')
6            return
7        res.append(root.val)
8        dfs(root.left)
9        dfs(root.right)
10    dfs(root)
11    return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4    StringJoiner res = new StringJoiner(" ");
5    serializeDFS(root, res);
6    return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10    if (root == null) {
11        result.add("x");
12        return;
13    }
14    result.add(Integer.toString(root.val));
15    serializeDFS(root.left, result);
16    serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2    let res = [];
3    serialize_dfs(root, res);
4    return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8    if (!root) {
9        res.push("x");
10        return;
11    }
12    res.push(root.val);
13    serialize_dfs(root.left, res);
14    serialize_dfs(root.right, res);
15}
16

Recommended Readings

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

Load More