1310. XOR Queries of a Subarray
Problem Description
In this problem, we are presented with two arrays: one called arr
, which holds positive integers, and another called queries
. Each element in queries
is a pair [left_i, right_i]
, where left_i
and right_i
are indices into the arr
. For each query pair, we need to calculate the XOR (exclusive OR) of all elements in arr
between these two indices, inclusive. The XOR of a sequence of numbers is a binary operation that takes two bits, returning 0 if the bits are the same and 1 if they are different. For a sequence of numbers, the XOR is applied in a pairwise fashion from left to right.
The XOR operation has a unique property where X XOR X
equals 0 for any number X
, and X XOR 0
equals X
. Another important property is that XOR operations can be performed in any order due to their associativity.
Therefore, the task is to return a new array, answer
, where each element answer[i]
is the result of the corresponding XOR operation from the i
th query.
Intuition
The straightforward approach to solving this problem would involve running through each query, and performing the XOR operation across the range of indices specified for every query. This would result in an algorithm that runs in O(n*m) time, where n is the length of arr
and m is the number of queries, which can be very slow if both are large.
The solution code uses a clever insight combined with a property of the XOR operation to avoid recomputing the XOR for overlapping ranges and to handle each query in constant time, resulting in a much more efficient algorithm.
The intuition behind the solution lies in precomputing a list s
that holds the cumulative XOR up to each index in arr
. The cumulative XOR s[i]
at index i
will be the XOR of all elements from arr[0]
to arr[i - 1]
. We start the accumulation with an initial value of 0 (since X XOR 0
equals X
for any number X
).
Once we have this precomputed cumulative XOR array, to find the XOR for any range [left_i, right_i]
, we can use the following observation:
The XOR of a range from left_i
to right_i
can be calculated by XOR-ing the cumulative XORs up to right_i
and left_i - 1
. This is because the cumulative XOR up to right_i
includes all the elements we want but also includes all the elements before left_i
that we don't want. By XORing with the cumulative XOR up to left_i - 1
, we cancel out the unwanted elements due to the property that X XOR X
equals 0.
Therefore, the answer for each query i
is s[right_i + 1] XOR s[left_i]
. The reason right_i + 1
is used instead of right_i
is to correctly handle the end index, because the cumulative XOR s[i]
is calculated up to, but not including, index i
.
Using this approach, we reduce the time complexity of answering all the queries to O(n + m), which is much more efficient, especially when dealing with a large number of queries.
Learn more about Prefix Sum patterns.
Solution Approach
The solution for the XOR query problem is built upon the efficient computation of multiple range XOR queries over an immutable array. To achieve this, we use a prefix XOR and a simple array traversal. Here's a step-by-step walk-through of the implementation:
-
Prefix XOR Computation: We initialize an array,
s
, to store the prefix XOR values. Prefix XOR at positioni
represents the XOR of all elements from the beginning of the arrayarr
up to thei-1
th position. We use Python'saccumulate
function from theitertools
module with the bitwise XOR operator, passing aninitial
value of 0 to include the XOR for the element at index 0 as well. -
Iterating Over Queries: We loop through each query in
queries
, which contains pairs of[l, r]
representing the range of indicesleft
toright
. -
Computing Range XOR: For each query
[l, r]
, we compute the XOR of the range by XOR-ing the prefix XOR valuess[r + 1]
ands[l]
. The reason we dos[r + 1]
instead ofs[r]
is because our prefix accumulation starts with a 0, offsetting each index by 1. The values[r + 1]
thus contains the XOR of all elements from 0 tor
inclusive. Since XOR is an associative and commutative operation, we can remove the prefix up tol - 1
(contained ins[l]
) from this cumulative value to get the XOR of elements froml
tor
. -
Appending Results: The result for the current query is appended to the list
answer
. This list will eventually contain the XOR results for all the queries.
The final solution code thus looks as follows:
class Solution:
def xorQueries(self, arr: List[int], queries: List[List[int]]) -> List[int]:
# Step 1: Compute the prefix XOR list `s`
s = list(accumulate(arr, xor, initial=0))
# Step 2 & 3: Process each query and compute the range XOR
# Step 4: Collect range XOR results into the final answer list
return [s[r + 1] ^ s[l] for l, r in queries]
By precomputing the cumulative XOR up to each point in the array and cleverly using the XOR properties, this approach answers each query in constant time after an initial preprocessing step, making the solution highly efficient.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's illustrate the solution approach with a small example. Suppose the input array arr = [4, 8, 2, 10]
and the queries are [ [0,1], [1,3], [2,3] ]
.
-
Prefix XOR Computation: We first build the cumulative
s
array that holds the XOR from start up to indexi-1
as follows:s[0]
is initialized to 0.s[1] = arr[0] XOR s[0] = 4 XOR 0 = 4
s[2] = arr[1] XOR s[1] = 8 XOR 4 = 12
s[3] = arr[2] XOR s[2] = 2 XOR 12 = 14
s[4] = arr[3] XOR s[3] = 10 XOR 14 = 4
So, our cumulative XOR array,
s
, now looks like[0, 4, 12, 14, 4]
. -
Iterating Over Queries: Now let's process each of the queries.
-
Computing Range XOR:
- For the first query
[0,1]
, we calculate the result ass[1+1] ^ s[0]
which iss[2] ^ s[0] = 12 XOR 0 = 12
. - For the second query
[1,3]
, the result iss[3+1] ^ s[1]
which iss[4] ^ s[1] = 4 XOR 4 = 0
. - For the third query
[2,3]
, the result iss[3+1] ^ s[2]
which iss[4] ^ s[2] = 4 XOR 12 = 8
.
- For the first query
-
Appending Results: We append each result to the
answer
list, which at the end of the iteration contains[12, 0, 8]
.
The output for the provided example is therefore [12, 0, 8]
, corresponding to the XOR of elements from:
- Indices
0
to1
: XOR of4
and8
is12
- Indices
1
to3
: XOR of8
,2
,10
is0
- Indices
2
to3
: XOR of2
and10
is8
By using a precomputed cumulative XOR array and understanding the associativity of the XOR operation, this approach efficiently computes the result for all queries without having to recompute the XOR from scratch for each query range.
Solution Implementation
1from itertools import accumulate
2from operator import xor
3
4class Solution:
5 def xorQueries(self, arr: List[int], queries: List[List[int]]) -> List[int]:
6 # Calculate the accumulated XOR values for the entire array.
7 # The initial value is 0 because 0 XOR with any number returns that number.
8 accumulated_xor = list(accumulate(arr, xor, initial=0))
9
10 # Process each query to get the XOR from arr[l] to arr[r].
11 # We utilize the property: XOR from arr[l] to arr[r] is
12 # accumulated_xor[r + 1] XOR accumulated_xor[l].
13 results = [accumulated_xor[r + 1] ^ accumulated_xor[l] for l, r in queries]
14
15 return results
16
1class Solution {
2 // Function to perform XOR queries on an array
3 public int[] xorQueries(int[] arr, int[][] queries) {
4 // Length of the original array
5 int n = arr.length;
6
7 // Initialize a prefix XOR array with an additional element to handle zero-indexing
8 int[] prefixXOR = new int[n + 1];
9
10 // Populate the prefix XOR array where each element is the XOR of all elements before it
11 for (int i = 1; i <= n; ++i) {
12 prefixXOR[i] = prefixXOR[i - 1] ^ arr[i - 1];
13 }
14
15 // Number of queries
16 int m = queries.length;
17
18 // Initialize the array to hold the results of the queries
19 int[] answer = new int[m];
20
21 // Iterate through each query
22 for (int i = 0; i < m; ++i) {
23 // Extract the left and right indices of the current query
24 int left = queries[i][0], right = queries[i][1];
25
26 // Calculate the XOR for the given range by using the prefix XOR array
27 // s[r + 1] gives the XOR from arr[0] to arr[r] inclusive
28 // s[l] gives the XOR from arr[0] to arr[l - 1] inclusive
29 // XORing these two gives the XOR from arr[l] to arr[r] inclusive, which is the answer for this query
30 answer[i] = prefixXOR[right + 1] ^ prefixXOR[left];
31 }
32
33 // Return the array containing the result of each query
34 return answer;
35 }
36}
37
1#include <vector>
2#include <cstring>
3
4using namespace std;
5
6class Solution {
7public:
8 // Function that returns the result of XOR queries on an array.
9 vector<int> xorQueries(vector<int>& arr, vector<vector<int>>& queries) {
10 // Find the size of the input array.
11 int arraySize = arr.size();
12
13 // Create an array to store the prefix XOR up to each element.
14 int prefixXor[arraySize + 1];
15
16 // Initialize the prefixXor array to 0.
17 memset(prefixXor, 0, sizeof(prefixXor));
18
19 // Compute the prefix XOR array, where prefixXor[i] stores the XOR of all elements up to index i-1.
20 for (int i = 1; i <= arraySize; ++i) {
21 prefixXor[i] = prefixXor[i - 1] ^ arr[i - 1];
22 }
23
24 // Create a vector to store the answers to the queries.
25 vector<int> answers;
26
27 // Iterate over each query.
28 for (auto& query : queries) {
29 // Extract the left and right indices from the current query.
30 int leftIndex = query[0], rightIndex = query[1];
31
32 // Calculate the XOR from leftIndex to rightIndex using the prefix XOR values.
33 // The result of XOR from leftIndex to rightIndex is prefixXor[rightIndex+1] ^ prefixXor[leftIndex]
34 // because prefixXor[leftIndex] contains the XOR of all elements up to leftIndex - 1.
35 answers.push_back(prefixXor[rightIndex + 1] ^ prefixXor[leftIndex]);
36 }
37
38 // Return the answers to the queries.
39 return answers;
40 }
41};
42
1// Function to perform XOR queries on an array.
2// For every query, which consists of a pair [left, right], the function
3// calculates the XOR of elements from arr[left] to arr[right].
4function xorQueries(arr: number[], queries: number[][]): number[] {
5 const arrLength = arr.length; // Get the length of the array.
6
7 // Create an array to store the prefix XORs with an additional 0 at the beginning.
8 const prefixXOR: number[] = new Array(arrLength + 1).fill(0);
9
10 // Calculate the prefix XOR values for the array.
11 for (let i = 0; i < arrLength; ++i) {
12 prefixXOR[i + 1] = prefixXOR[i] ^ arr[i];
13 }
14
15 // Initialize an array to hold the result of each query.
16 const results: number[] = [];
17
18 // Process each query and calculate the XOR for the given range.
19 for (const [left, right] of queries) {
20 // XOR between the prefix XORs gives the XOR of the range.
21 results.push(prefixXOR[right + 1] ^ prefixXOR[left]);
22 }
23
24 // Return the array of results.
25 return results;
26}
27
Time and Space Complexity
Time Complexity
The time complexity of the provided code consists of two parts: the computation of the prefix xors and the processing of the queries.
-
Computing the prefix xors with
accumulate
is an O(n) operation, wheren
is the number of elements in the arrayarr
. This is because it processes each element of the array exactly once with thexor
operation. -
The list comprehension iterates through each query and performs a constant-time xor operation. If we have
m
queries, the time to process all queries will be O(m).
Combining both parts, the overall time complexity is O(n + m), where n
is the length of arr
and m
is the number of queries.
Space Complexity
The space complexity of the provided code can be analyzed as follows:
-
The space taken by the prefix xors list
s
is O(n), wheren
is the size of the input arrayarr
. -
The space for the output list is O(m), where
m
is the number of queries.
Therefore, the total space complexity is O(n + m), which accounts for the space occupied by the prefix xors and the output list.
Learn more about how to find time and space complexity quickly using problem constraints.
What's the output of running the following function using input 56
?
1KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13 def dfs(path, res):
14 if len(path) == len(digits):
15 res.append(''.join(path))
16 return
17
18 next_number = digits[len(path)]
19 for letter in KEYBOARD[next_number]:
20 path.append(letter)
21 dfs(path, res)
22 path.pop()
23
24 res = []
25 dfs([], res)
26 return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2 '2', "abc".toCharArray(),
3 '3', "def".toCharArray(),
4 '4', "ghi".toCharArray(),
5 '5', "jkl".toCharArray(),
6 '6', "mno".toCharArray(),
7 '7', "pqrs".toCharArray(),
8 '8', "tuv".toCharArray(),
9 '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13 List<String> res = new ArrayList<>();
14 dfs(new StringBuilder(), res, digits.toCharArray());
15 return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19 if (path.length() == digits.length) {
20 res.add(path.toString());
21 return;
22 }
23 char next_digit = digits[path.length()];
24 for (char letter : KEYBOARD.get(next_digit)) {
25 path.append(letter);
26 dfs(path, res, digits);
27 path.deleteCharAt(path.length() - 1);
28 }
29}
30
1const KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13 let res = [];
14 dfs(digits, [], res);
15 return res;
16}
17
18function dfs(digits, path, res) {
19 if (path.length === digits.length) {
20 res.push(path.join(''));
21 return;
22 }
23 let next_number = digits.charAt(path.length);
24 for (let letter of KEYBOARD[next_number]) {
25 path.push(letter);
26 dfs(digits, path, res);
27 path.pop();
28 }
29}
30
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
LeetCode 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 we
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 algomonster s3 us east 2 amazonaws com recursion jpg You first
Want a Structured Path to Master System Design Too? Don’t Miss This!