You are given an integer array arr
and an array queries
where each queries[i] = [L_i, R_i]
represents a subarray from index L_i
to R_i
(inclusive). For each query, compute the bitwise XOR of the elements in the subarray arr[L_i] ^ arr[L_i+1] ^ ... ^ arr[R_i]
.
arr
: a list of integersqueries
: a list of pairs [L_i, R_i]
i
-th integer is the XOR of the subarray defined by queries[i]
Constraints:
1 <= arr.length <= 3 * 10^4
1 <= queries.length <= 3 * 10^4
0 <= arr[i] <= 10^9
0 <= L_i <= R_i < arr.length
At first glance, the most straightforward way to solve this problem is to compute the XOR for each query by iterating through the specified subarray. For each query [L_i, R_i]
, we would loop from L_i
to R_i
and XOR all the elements. While this works, it is inefficient if there are many queries or the subarrays are large, leading to a time complexity of O(Q * N)
where Q
is the number of queries and N
is the array length.
To optimize, we should look for patterns or data structures that let us compute the XOR of any subarray in constant time. One key property of XOR is that it is associative and its own inverse: a ^ a = 0
and a ^ 0 = a
. This suggests we can use prefix XORs, much like prefix sums for sum queries.
To efficiently answer multiple XOR subarray queries, we use a prefix XOR array. Here’s how:
prefix
where prefix[0] = 0
.i
(1-based), prefix[i] = prefix[i-1] ^ arr[i-1]
.prefix[i]
holds the XOR of all elements from arr[0]
to arr[i-1]
.[L, R]
, the XOR of arr[L]...arr[R]
is prefix[R+1] ^ prefix[L]
.L-1
, leaving just the XOR of the subarray.prefix[R+1] ^ prefix[L]
and store the result.
This approach preprocesses the array in O(N)
time and answers each query in O(1)
, making the total time O(N + Q)
.
Input:
arr = [1, 3, 4, 8]
queries = [[0,1],[1,2],[0,3],[3,3]]
prefix[0] = 0
prefix[1] = 0 ^ 1 = 1
prefix[2] = 1 ^ 3 = 2
prefix[3] = 2 ^ 4 = 6
prefix[4] = 6 ^ 8 = 14
So prefix = [0, 1, 2, 6, 14]
[0,1]
: prefix[2] ^ prefix[0] = 2 ^ 0 = 2
(1^3)[1,2]
: prefix[3] ^ prefix[1] = 6 ^ 1 = 7
(3^4)[0,3]
: prefix[4] ^ prefix[0] = 14 ^ 0 = 14
(1^3^4^8)[3,3]
: prefix[4] ^ prefix[3] = 14 ^ 6 = 8
(8)
Output: [2, 7, 14, 8]
class Solution:
def xorQueries(self, arr, queries):
prefix = [0]
for num in arr:
prefix.append(prefix[-1] ^ num)
result = []
for l, r in queries:
result.append(prefix[r+1] ^ prefix[l])
return result
class Solution {
public:
vector<int> xorQueries(vector<int>& arr, vector<vector<int>>& queries) {
vector<int> prefix(arr.size() + 1, 0);
for (int i = 0; i < arr.size(); ++i) {
prefix[i+1] = prefix[i] ^ arr[i];
}
vector<int> result;
for (auto& q : queries) {
int l = q[0], r = q[1];
result.push_back(prefix[r+1] ^ prefix[l]);
}
return result;
}
};
class Solution {
public int[] xorQueries(int[] arr, int[][] queries) {
int n = arr.length;
int[] prefix = new int[n + 1];
for (int i = 0; i < n; i++) {
prefix[i+1] = prefix[i] ^ arr[i];
}
int[] res = new int[queries.length];
for (int i = 0; i < queries.length; i++) {
int l = queries[i][0], r = queries[i][1];
res[i] = prefix[r+1] ^ prefix[l];
}
return res;
}
}
var xorQueries = function(arr, queries) {
let prefix = [0];
for (let num of arr) {
prefix.push(prefix[prefix.length - 1] ^ num);
}
let res = [];
for (let [l, r] of queries) {
res.push(prefix[r + 1] ^ prefix[l]);
}
return res;
};
O(Q * N)
in the worst case, where Q
is the number of queries and N
is the array length.O(1)
extra (not counting output).O(N)
O(1)
O(N + Q)
O(N)
for the prefix arrayThe optimized approach is much faster and well-suited for large inputs.
This problem demonstrates how prefix computations can transform repeated, expensive operations into constant-time lookups. By leveraging the properties of XOR and building a prefix XOR array, we can answer any subarray XOR query in constant time after a single linear pass over the input. This technique is efficient, easy to implement, and highlights a powerful pattern for range query problems.