1310. XOR Queries of a Subarray
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.
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 rs[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 EvaluatorExample 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
takesO(n)
time as it processes each element ofarr
once. - Processing all queries takes
O(m)
time since each query is answered inO(1)
time using the precomputed prefix XOR values.
The space complexity is O(n)
.
- The prefix XOR array
s
storesn + 1
elements (including the initial value 0), which requiresO(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 returnarr[0]
- Query
[n-1, n-1]
should returnarr[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
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
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
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!